From bf5589b88d4950d8d0d899d8b3a1f6eb6e642a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 8 Sep 2022 02:59:08 +0200 Subject: [PATCH 01/98] Guard more of the hardware panel (#13650) --- .../config/hardware/ha-config-hardware.ts | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 3e19697d1e..2d225bca1c 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -80,29 +80,31 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { private _cpuEntries: { x: number; y: number | null }[] = []; public hassSubscribe() { - return [ - this.hass.connection.subscribeMessage( - (message) => { - // Only store the last 60 entries - this._memoryEntries.shift(); - this._cpuEntries.shift(); + return isComponentLoaded(this.hass, "hardware") + ? [ + this.hass.connection.subscribeMessage( + (message) => { + // Only store the last 60 entries + this._memoryEntries.shift(); + this._cpuEntries.shift(); - this._memoryEntries.push({ - x: new Date(message.timestamp).getTime(), - y: message.memory_used_percent, - }); - this._cpuEntries.push({ - x: new Date(message.timestamp).getTime(), - y: message.cpu_percent, - }); + this._memoryEntries.push({ + x: new Date(message.timestamp).getTime(), + y: message.memory_used_percent, + }); + this._cpuEntries.push({ + x: new Date(message.timestamp).getTime(), + y: message.cpu_percent, + }); - this._systemStatusData = message; - }, - { - type: "hardware/subscribe_system_status", - } - ), - ]; + this._systemStatusData = message; + }, + { + type: "hardware/subscribe_system_status", + } + ), + ] + : []; } protected willUpdate(): void { @@ -198,32 +200,34 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { .narrow=${this.narrow} .header=${this.hass.localize("ui.panel.config.hardware.caption")} > - - - ${this.hass.localize( - "ui.panel.config.hardware.available_hardware.title" - )} - ${this._hostData - ? html` - ${this.hass.localize( - "ui.panel.config.hardware.reboot_host" - )} - ${this.hass.localize( - "ui.panel.config.hardware.shutdown_host" - )} - ` - : ""} - + ${isComponentLoaded(this.hass, "hassio") + ? html` + + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.title" + )} + ${this._hostData + ? html` + ${this.hass.localize( + "ui.panel.config.hardware.reboot_host" + )} + ${this.hass.localize( + "ui.panel.config.hardware.shutdown_host" + )} + ` + : ""} + ` + : ""} ${this._error ? html` Date: Thu, 8 Sep 2022 12:00:52 +0200 Subject: [PATCH 02/98] Move automation trace actions to overflow menu (#13656) --- .../config/automation/ha-automation-trace.ts | 88 +++++++++++++++---- src/panels/config/script/ha-script-trace.ts | 87 ++++++++++++++---- 2 files changed, 140 insertions(+), 35 deletions(-) diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index b2884f064f..31f795f801 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -1,5 +1,8 @@ import { + mdiDotsVertical, mdiDownload, + mdiInformationOutline, + mdiPencil, mdiRayEndArrow, mdiRayStartArrow, mdiRefresh, @@ -10,6 +13,8 @@ import { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button-menu"; import "../../../components/ha-icon-button"; import "../../../components/trace/ha-trace-blueprint-config"; import "../../../components/trace/ha-trace-config"; @@ -31,9 +36,9 @@ import { loadTraces, } from "../../../data/trace"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; -import "../../../layouts/hass-subpage"; @customElement("ha-automation-trace") export class HaAutomationTrace extends LitElement { @@ -107,23 +112,63 @@ export class HaAutomationTrace extends LitElement { ` : ""} - - + + + + + ${this.hass.localize("ui.panel.config.automation.editor.show_info")} + + + + ${stateObj?.attributes.id && this.narrow + ? html` + + + ${this.hass.localize( + "ui.panel.config.automation.trace.edit_automation" + )} + + + + ` + : ""} + +
  • + + + ${this.hass.localize("ui.panel.config.automation.trace.refresh")} + + + + + ${this.hass.localize( + "ui.panel.config.automation.trace.download_trace" + )} + + +
    +
    ${this._traces && this._traces.length > 0 ? html` @@ -443,6 +488,13 @@ export class HaAutomationTrace extends LitElement { } } + private _showInfo() { + if (!this.hass || !this._entityId) { + return; + } + fireEvent(this, "hass-more-info", { entityId: this._entityId }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index e0ba6c6d33..ab35977cb1 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -1,5 +1,8 @@ import { + mdiDotsVertical, mdiDownload, + mdiInformationOutline, + mdiPencil, mdiRayEndArrow, mdiRayStartArrow, mdiRefresh, @@ -34,6 +37,8 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import "../../../layouts/hass-subpage"; +import "../../../components/ha-button-menu"; +import { fireEvent } from "../../../common/dom/fire_event"; @customElement("ha-script-trace") export class HaScriptTrace extends LitElement { @@ -105,23 +110,64 @@ export class HaScriptTrace extends LitElement { ` : ""} - - + + + + + + ${this.hass.localize("ui.panel.config.script.editor.show_info")} + + + + ${this.narrow && this.scriptEntityId + ? html` + + + ${this.hass.localize( + "ui.panel.config.script.trace.edit_script" + )} + + + + ` + : ""} + +
  • + + + ${this.hass.localize("ui.panel.config.automation.trace.refresh")} + + + + + ${this.hass.localize( + "ui.panel.config.automation.trace.download_trace" + )} + + +
    +
    ${this._traces && this._traces.length > 0 ? html` @@ -429,6 +475,13 @@ export class HaScriptTrace extends LitElement { } } + private async _showInfo() { + if (!this.scriptEntityId) { + return; + } + fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); + } + static get styles(): CSSResultGroup { return [ haStyle, From 462dee03510c24a51af1682cb7e058b514b38f29 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 8 Sep 2022 17:43:36 +0200 Subject: [PATCH 03/98] use input instead of change (#13660) --- .../automation-rename-dialog/dialog-automation-rename.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts index e97976e3f7..8dcf1e4219 100644 --- a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts +++ b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts @@ -79,7 +79,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { )} required type="string" - @change=${this._valueChanged} + @input=${this._valueChanged} > From 07b5856190b1442ce72365bb5e299165e9450f1b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 Sep 2022 22:03:35 +0200 Subject: [PATCH 04/98] Don't call deprecated history/* statistics API (#13658) --- src/data/history.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 8337dead32..b314a3eaef 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -457,7 +457,7 @@ export const getStatisticIds = ( statistic_type?: "mean" | "sum" ) => hass.callWS({ - type: "history/list_statistic_ids", + type: "recorder/list_statistic_ids", statistic_type, }); @@ -478,7 +478,7 @@ export const fetchStatistics = ( period: "5minute" | "hour" | "day" | "month" = "hour" ) => hass.callWS({ - type: "history/statistics_during_period", + type: "recorder/statistics_during_period", start_time: startTime.toISOString(), end_time: endTime?.toISOString(), statistic_ids, From d323ab672630774c3a2371d2d64a23acb6569127 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Sep 2022 10:22:26 -0500 Subject: [PATCH 05/98] Add support for subscribing to config entry changes (#13585) --- src/data/config_entries.ts | 24 +++++ .../dialog-config-entry-system-options.ts | 1 - ...show-dialog-config-entry-system-options.ts | 1 - .../integrations/ha-config-integrations.ts | 102 +++++++++--------- .../integrations/ha-integration-card.ts | 23 ++-- 5 files changed, 86 insertions(+), 65 deletions(-) diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 5a3757b8e4..42124f53d6 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -1,3 +1,4 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { HomeAssistant } from "../types"; export interface ConfigEntry { @@ -44,6 +45,29 @@ export const RECOVERABLE_STATES: ConfigEntry["state"][] = [ "setup_retry", ]; +export interface ConfigEntryUpdate { + // null means no update as is the current state + type: null | "added" | "removed" | "updated"; + entry: ConfigEntry; +} + +export const subscribeConfigEntries = ( + hass: HomeAssistant, + callbackFunction: (message: ConfigEntryUpdate[]) => void, + filters?: { type?: "helper" | "integration"; domain?: string } +): Promise => { + const params: any = { + type: "config_entries/subscribe", + }; + if (filters && filters.type) { + params.type_filter = filters.type; + } + return hass.connection.subscribeMessage( + (message) => callbackFunction(message), + params + ); +}; + export const getConfigEntries = ( hass: HomeAssistant, filters?: { type?: "helper" | "integration"; domain?: string } diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index aaced030d6..8329685a6e 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -170,7 +170,6 @@ class DialogConfigEntrySystemOptions extends LitElement { ), }); } - this._params!.entryUpdated(result.config_entry); this.closeDialog(); } catch (err: any) { this._error = err.message || "Unknown error"; diff --git a/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts index 77415e02cd..0ea3b9d966 100644 --- a/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/show-dialog-config-entry-system-options.ts @@ -5,7 +5,6 @@ import { IntegrationManifest } from "../../data/integration"; export interface ConfigEntrySystemOptionsDialogParams { entry: ConfigEntry; manifest?: IntegrationManifest; - entryUpdated(entry: ConfigEntry): void; } export const loadConfigEntrySystemOptionsDialog = () => diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 82ce8bf60f..3ec709d2c5 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -14,7 +14,7 @@ import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent } from "../../../common/dom/fire_event"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; @@ -28,7 +28,10 @@ import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import "../../../components/search-input"; -import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; +import { + ConfigEntry, + subscribeConfigEntries, +} from "../../../data/config_entries"; import { getConfigFlowHandlers, getConfigFlowInProgressCollection, @@ -151,7 +154,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { @state() private _diagnosticHandlers?: Record; - public hassSubscribe(): UnsubscribeFunc[] { + public hassSubscribe(): Array> { return [ subscribeEntityRegistry(this.hass.connection, (entries) => { this._entityRegistryEntries = entries; @@ -180,6 +183,53 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { localized_title: localizeConfigFlowTitle(this.hass.localize, flow), })); }), + subscribeConfigEntries( + this.hass, + (messages) => { + let fullUpdate = false; + const newEntries: ConfigEntryExtended[] = []; + messages.forEach((message) => { + if (message.type === null || message.type === "added") { + newEntries.push({ + ...message.entry, + localized_domain_name: domainToName( + this.hass.localize, + message.entry.domain + ), + }); + if (message.type === null) { + fullUpdate = true; + } + } else if (message.type === "removed") { + this._configEntries = this._configEntries!.filter( + (entry) => entry.entry_id !== message.entry.entry_id + ); + } else if (message.type === "updated") { + const newEntry = message.entry; + this._configEntries = this._configEntries!.map((entry) => + entry.entry_id === newEntry.entry_id + ? { + ...newEntry, + localized_domain_name: entry.localized_domain_name, + } + : entry + ); + } + }); + if (!newEntries.length && !fullUpdate) { + return; + } + const existingEntries = fullUpdate ? [] : this._configEntries; + this._configEntries = [...existingEntries!, ...newEntries].sort( + (conf1, conf2) => + caseInsensitiveStringCompare( + conf1.localized_domain_name + conf1.title, + conf2.localized_domain_name + conf2.title + ) + ); + }, + { type: "integration" } + ), ]; } @@ -257,7 +307,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { protected firstUpdated(changed: PropertyValues) { super.firstUpdated(changed); - this._loadConfigEntries(); const localizePromise = this.hass.loadBackendTranslation( "title", undefined, @@ -411,11 +460,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
    `} -
    +
    ${this._showIgnored ? ignoredConfigEntries.map( (entry: ConfigEntryExtended) => html` @@ -542,29 +587,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { ev.preventDefault(); } - private _loadConfigEntries() { - getConfigEntries(this.hass, { type: "integration" }).then( - (configEntries) => { - this._configEntries = configEntries - .map( - (entry: ConfigEntry): ConfigEntryExtended => ({ - ...entry, - localized_domain_name: domainToName( - this.hass.localize, - entry.domain - ), - }) - ) - .sort((conf1, conf2) => - caseInsensitiveStringCompare( - conf1.localized_domain_name + conf1.title, - conf2.localized_domain_name + conf2.title - ) - ); - } - ); - } - private async _scanUSBDevices() { if (!isComponentLoaded(this.hass, "usb")) { return; @@ -602,23 +624,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } } - private _handleEntryRemoved(ev: HASSDomEvent) { - this._configEntries = this._configEntries!.filter( - (entry) => entry.entry_id !== ev.detail.entryId - ); - } - - private _handleEntryUpdated(ev: HASSDomEvent) { - const newEntry = ev.detail.entry; - this._configEntries = this._configEntries!.map((entry) => - entry.entry_id === newEntry.entry_id - ? { ...newEntry, localized_domain_name: entry.localized_domain_name } - : entry - ); - } - private _handleFlowUpdated() { - this._loadConfigEntries(); getConfigFlowInProgressCollection(this.hass.connection).refresh(); this._fetchManifests(); } diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 843bf2223c..59d61f189a 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -11,6 +11,7 @@ import { mdiDotsVertical, mdiDownload, mdiOpenInNew, + mdiReloadAlert, mdiProgressHelper, mdiPlayCircleOutline, mdiReload, @@ -24,7 +25,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../common/dom/fire_event"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; @@ -184,7 +184,9 @@ export class HaIntegrationCard extends LitElement { ? html` ${this.hass.localize( @@ -231,6 +233,9 @@ export class HaIntegrationCard extends LitElement { "ui.panel.config.integrations.config_entry.setup_in_progress", ]; } else if (ERROR_STATES.includes(item.state)) { + if (item.state === "setup_retry") { + icon = mdiReloadAlert; + } stateText = [ `ui.panel.config.integrations.config_entry.state.${item.state}`, ]; @@ -622,10 +627,6 @@ export class HaIntegrationCard extends LitElement { showConfigEntrySystemOptionsDialog(this, { entry: configEntry, manifest: this.manifest, - entryUpdated: (entry) => - fireEvent(this, "entry-updated", { - entry, - }), }); } @@ -660,9 +661,6 @@ export class HaIntegrationCard extends LitElement { ), }); } - fireEvent(this, "entry-updated", { - entry: { ...configEntry, disabled_by: "user" }, - }); } private async _enableIntegration(configEntry: ConfigEntry) { @@ -688,9 +686,6 @@ export class HaIntegrationCard extends LitElement { ), }); } - fireEvent(this, "entry-updated", { - entry: { ...configEntry, disabled_by: null }, - }); } private async _removeIntegration(configEntry: ConfigEntry) { @@ -707,7 +702,6 @@ export class HaIntegrationCard extends LitElement { return; } const result = await deleteConfigEntry(this.hass, entryId); - fireEvent(this, "entry-removed", { entryId }); if (result.require_restart) { showAlertDialog(this, { @@ -743,10 +737,9 @@ export class HaIntegrationCard extends LitElement { if (newName === null) { return; } - const result = await updateConfigEntry(this.hass, configEntry.entry_id, { + await updateConfigEntry(this.hass, configEntry.entry_id, { title: newName, }); - fireEvent(this, "entry-updated", { entry: result.config_entry }); } private async _signUrl(ev) { From 47c0901df2b0c9851a1c3647cf06b333c6d40077 Mon Sep 17 00:00:00 2001 From: krazos Date: Mon, 12 Sep 2022 05:57:53 -0400 Subject: [PATCH 06/98] Eliminate redundant "for" in trigger description (#13669) --- src/data/automation_i18n.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 1c7e8a1537..93a6970cc9 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -143,11 +143,11 @@ export const describeTrigger = ( if ("for" in trigger) { let duration: string; if (typeof trigger.for === "number") { - duration = `for ${secondsToDuration(trigger.for)!}`; + duration = `${secondsToDuration(trigger.for)!}`; } else if (typeof trigger.for === "string") { - duration = `for ${trigger.for}`; + duration = `${trigger.for}`; } else { - duration = `for ${JSON.stringify(trigger.for)}`; + duration = `${JSON.stringify(trigger.for)}`; } base += ` for ${duration}`; From 1086c859640e5e4af9ae42588c61d59df5c42229 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Mon, 12 Sep 2022 07:22:49 -0300 Subject: [PATCH 07/98] Use newspaper icon for change log button (#13668) --- src/panels/config/info/ha-config-info.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index 3c87a70dd7..bb4634a000 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -5,7 +5,7 @@ import { mdiHandsPray, mdiHelp, mdiHomeAssistant, - mdiPower, + mdiNewspaperVariant, mdiTshirtCrew, } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -36,7 +36,7 @@ const PAGES: Array<{ { name: "change_log", path: "/latest-release-notes/", - iconPath: mdiPower, + iconPath: mdiNewspaperVariant, iconColor: "#4A5963", }, { From d5d6216cfe340db63cf321dd13df6a97f1cee8f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 Sep 2022 13:23:02 +0200 Subject: [PATCH 08/98] Move recorder statistics API to data/recorder.ts (#13672) * Move recorder statistics API to data/recorder.ts * Fix import * prettier --- demo/src/stubs/history.ts | 2 +- src/components/chart/statistics-chart.ts | 2 +- src/components/entity/ha-statistic-picker.ts | 2 +- src/data/energy.ts | 2 +- src/data/history.ts | 215 +----------------- src/data/recorder.ts | 210 +++++++++++++++++ .../components/ha-energy-battery-settings.ts | 2 +- .../components/ha-energy-device-settings.ts | 2 +- .../components/ha-energy-gas-settings.ts | 2 +- .../components/ha-energy-grid-settings.ts | 2 +- .../components/ha-energy-solar-settings.ts | 2 +- src/panels/config/energy/ha-config-energy.ts | 2 +- .../statistics/developer-tools-statistics.ts | 2 +- .../dialog-statistics-adjust-sum.ts | 2 +- .../dialog-statistics-fix-units-changed.ts | 2 +- ...og-statistics-fix-unsupported-unit-meta.ts | 2 +- .../show-dialog-statistics-adjust-sum.ts | 2 +- ...how-dialog-statistics-fix-units-changed.ts | 2 +- ...og-statistics-fix-unsupported-unit-meta.ts | 2 +- .../hui-energy-carbon-consumed-gauge-card.ts | 2 +- .../energy/hui-energy-devices-graph-card.ts | 2 +- .../energy/hui-energy-distribution-card.ts | 2 +- .../cards/energy/hui-energy-gas-graph-card.ts | 2 +- .../hui-energy-grid-neutrality-gauge-card.ts | 2 +- .../hui-energy-solar-consumed-gauge-card.ts | 2 +- .../energy/hui-energy-solar-graph-card.ts | 2 +- .../energy/hui-energy-sources-table-card.ts | 2 +- .../energy/hui-energy-usage-graph-card.ts | 2 +- .../cards/hui-statistics-graph-card.ts | 2 +- src/panels/lovelace/cards/types.ts | 2 +- 30 files changed, 239 insertions(+), 242 deletions(-) create mode 100644 src/data/recorder.ts diff --git a/demo/src/stubs/history.ts b/demo/src/stubs/history.ts index 2e2c507ccc..86f3445956 100644 --- a/demo/src/stubs/history.ts +++ b/demo/src/stubs/history.ts @@ -6,7 +6,7 @@ import { endOfDay, } from "date-fns/esm"; import { HassEntity } from "home-assistant-js-websocket"; -import { StatisticValue } from "../../../src/data/history"; +import { StatisticValue } from "../../../src/data/recorder"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; interface HistoryQueryParams { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index ece8671a8c..50f7e263de 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -26,7 +26,7 @@ import { statisticsHaveType, StatisticsMetaData, StatisticType, -} from "../../data/history"; +} from "../../data/recorder"; import type { HomeAssistant } from "../../types"; import "./ha-chart-base"; diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index a2533c7bb9..1cfabba3d5 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -6,7 +6,7 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; import { stringCompare } from "../../common/string/compare"; -import { getStatisticIds, StatisticsMetaData } from "../../data/history"; +import { getStatisticIds, StatisticsMetaData } from "../../data/recorder"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; diff --git a/src/data/energy.ts b/src/data/energy.ts index f9a2c3d862..4c9f4f962d 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -20,7 +20,7 @@ import { getStatisticMetadata, Statistics, StatisticsMetaData, -} from "./history"; +} from "./recorder"; const energyCollectionKeys: (string | undefined)[] = []; diff --git a/src/data/history.ts b/src/data/history.ts index b314a3eaef..3f8777588a 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -1,10 +1,7 @@ import { HassEntities, HassEntity } from "home-assistant-js-websocket"; import { computeDomain } from "../common/entity/compute_domain"; import { computeStateDisplayFromEntityAttributes } from "../common/entity/compute_state_display"; -import { - computeStateName, - computeStateNameFromEntityAttributes, -} from "../common/entity/compute_state_name"; +import { computeStateNameFromEntityAttributes } from "../common/entity/compute_state_name"; import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; import { FrontendLocaleData } from "./translation"; @@ -63,87 +60,6 @@ export interface HistoryResult { timeline: TimelineEntity[]; } -export type StatisticType = "sum" | "min" | "max" | "mean"; - -export interface Statistics { - [statisticId: string]: StatisticValue[]; -} - -export interface StatisticValue { - statistic_id: string; - start: string; - end: string; - last_reset: string | null; - max: number | null; - mean: number | null; - min: number | null; - sum: number | null; - state: number | null; -} - -export interface StatisticsMetaData { - display_unit_of_measurement: string; - statistics_unit_of_measurement: string; - statistic_id: string; - source: string; - name?: string | null; - has_sum: boolean; - has_mean: boolean; -} - -export type StatisticsValidationResult = - | StatisticsValidationResultNoState - | StatisticsValidationResultEntityNotRecorded - | StatisticsValidationResultEntityNoLongerRecorded - | StatisticsValidationResultUnsupportedStateClass - | StatisticsValidationResultUnitsChanged - | StatisticsValidationResultUnsupportedUnitMetadata - | StatisticsValidationResultUnsupportedUnitState; - -export interface StatisticsValidationResultNoState { - type: "no_state"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultEntityNoLongerRecorded { - type: "entity_no_longer_recorded"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultEntityNotRecorded { - type: "entity_not_recorded"; - data: { statistic_id: string }; -} - -export interface StatisticsValidationResultUnsupportedStateClass { - type: "unsupported_state_class"; - data: { statistic_id: string; state_class: string }; -} - -export interface StatisticsValidationResultUnitsChanged { - type: "units_changed"; - data: { statistic_id: string; state_unit: string; metadata_unit: string }; -} - -export interface StatisticsValidationResultUnsupportedUnitMetadata { - type: "unsupported_unit_metadata"; - data: { - statistic_id: string; - device_class: string; - metadata_unit: string; - supported_unit: string; - }; -} - -export interface StatisticsValidationResultUnsupportedUnitState { - type: "unsupported_unit_state"; - data: { statistic_id: string; device_class: string; metadata_unit: string }; -} - -export interface StatisticsValidationResults { - [statisticId: string]: StatisticsValidationResult[]; -} - export interface HistoryStates { [entityId: string]: EntityHistoryState[]; } @@ -449,132 +365,3 @@ export const computeHistory = ( return { line: unitStates, timeline: timelineDevices }; }; - -// Statistics - -export const getStatisticIds = ( - hass: HomeAssistant, - statistic_type?: "mean" | "sum" -) => - hass.callWS({ - type: "recorder/list_statistic_ids", - statistic_type, - }); - -export const getStatisticMetadata = ( - hass: HomeAssistant, - statistic_ids?: string[] -) => - hass.callWS({ - type: "recorder/get_statistics_metadata", - statistic_ids, - }); - -export const fetchStatistics = ( - hass: HomeAssistant, - startTime: Date, - endTime?: Date, - statistic_ids?: string[], - period: "5minute" | "hour" | "day" | "month" = "hour" -) => - hass.callWS({ - type: "recorder/statistics_during_period", - start_time: startTime.toISOString(), - end_time: endTime?.toISOString(), - statistic_ids, - period, - }); - -export const validateStatistics = (hass: HomeAssistant) => - hass.callWS({ - type: "recorder/validate_statistics", - }); - -export const updateStatisticsMetadata = ( - hass: HomeAssistant, - statistic_id: string, - unit_of_measurement: string | null -) => - hass.callWS({ - type: "recorder/update_statistics_metadata", - statistic_id, - unit_of_measurement, - }); - -export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) => - hass.callWS({ - type: "recorder/clear_statistics", - statistic_ids, - }); - -export const calculateStatisticSumGrowth = ( - values: StatisticValue[] -): number | null => { - if (!values || values.length < 2) { - return null; - } - const endSum = values[values.length - 1].sum; - if (endSum === null) { - return null; - } - const startSum = values[0].sum; - if (startSum === null) { - return endSum; - } - return endSum - startSum; -}; - -export const calculateStatisticsSumGrowth = ( - data: Statistics, - stats: string[] -): number | null => { - let totalGrowth: number | null = null; - - for (const stat of stats) { - if (!(stat in data)) { - continue; - } - const statGrowth = calculateStatisticSumGrowth(data[stat]); - - if (statGrowth === null) { - continue; - } - if (totalGrowth === null) { - totalGrowth = statGrowth; - } else { - totalGrowth += statGrowth; - } - } - - return totalGrowth; -}; - -export const statisticsHaveType = ( - stats: StatisticValue[], - type: StatisticType -) => stats.some((stat) => stat[type] !== null); - -export const adjustStatisticsSum = ( - hass: HomeAssistant, - statistic_id: string, - start_time: string, - adjustment: number -): Promise => - hass.callWS({ - type: "recorder/adjust_sum_statistics", - statistic_id, - start_time, - adjustment, - }); - -export const getStatisticLabel = ( - hass: HomeAssistant, - statisticsId: string, - statisticsMetaData: StatisticsMetaData | undefined -): string => { - const entity = hass.states[statisticsId]; - if (entity) { - return computeStateName(entity); - } - return statisticsMetaData?.name || statisticsId; -}; diff --git a/src/data/recorder.ts b/src/data/recorder.ts new file mode 100644 index 0000000000..4022b83042 --- /dev/null +++ b/src/data/recorder.ts @@ -0,0 +1,210 @@ +import { computeStateName } from "../common/entity/compute_state_name"; +import { HomeAssistant } from "../types"; + +export type StatisticType = "sum" | "min" | "max" | "mean"; + +export interface Statistics { + [statisticId: string]: StatisticValue[]; +} + +export interface StatisticValue { + statistic_id: string; + start: string; + end: string; + last_reset: string | null; + max: number | null; + mean: number | null; + min: number | null; + sum: number | null; + state: number | null; +} + +export interface StatisticsMetaData { + display_unit_of_measurement: string; + statistics_unit_of_measurement: string; + statistic_id: string; + source: string; + name?: string | null; + has_sum: boolean; + has_mean: boolean; +} + +export type StatisticsValidationResult = + | StatisticsValidationResultNoState + | StatisticsValidationResultEntityNotRecorded + | StatisticsValidationResultEntityNoLongerRecorded + | StatisticsValidationResultUnsupportedStateClass + | StatisticsValidationResultUnitsChanged + | StatisticsValidationResultUnsupportedUnitMetadata + | StatisticsValidationResultUnsupportedUnitState; + +export interface StatisticsValidationResultNoState { + type: "no_state"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultEntityNoLongerRecorded { + type: "entity_no_longer_recorded"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultEntityNotRecorded { + type: "entity_not_recorded"; + data: { statistic_id: string }; +} + +export interface StatisticsValidationResultUnsupportedStateClass { + type: "unsupported_state_class"; + data: { statistic_id: string; state_class: string }; +} + +export interface StatisticsValidationResultUnitsChanged { + type: "units_changed"; + data: { statistic_id: string; state_unit: string; metadata_unit: string }; +} + +export interface StatisticsValidationResultUnsupportedUnitMetadata { + type: "unsupported_unit_metadata"; + data: { + statistic_id: string; + device_class: string; + metadata_unit: string; + supported_unit: string; + }; +} + +export interface StatisticsValidationResultUnsupportedUnitState { + type: "unsupported_unit_state"; + data: { statistic_id: string; device_class: string; metadata_unit: string }; +} + +export interface StatisticsValidationResults { + [statisticId: string]: StatisticsValidationResult[]; +} + +export const getStatisticIds = ( + hass: HomeAssistant, + statistic_type?: "mean" | "sum" +) => + hass.callWS({ + type: "recorder/list_statistic_ids", + statistic_type, + }); + +export const getStatisticMetadata = ( + hass: HomeAssistant, + statistic_ids?: string[] +) => + hass.callWS({ + type: "recorder/get_statistics_metadata", + statistic_ids, + }); + +export const fetchStatistics = ( + hass: HomeAssistant, + startTime: Date, + endTime?: Date, + statistic_ids?: string[], + period: "5minute" | "hour" | "day" | "month" = "hour" +) => + hass.callWS({ + type: "recorder/statistics_during_period", + start_time: startTime.toISOString(), + end_time: endTime?.toISOString(), + statistic_ids, + period, + }); + +export const validateStatistics = (hass: HomeAssistant) => + hass.callWS({ + type: "recorder/validate_statistics", + }); + +export const updateStatisticsMetadata = ( + hass: HomeAssistant, + statistic_id: string, + unit_of_measurement: string | null +) => + hass.callWS({ + type: "recorder/update_statistics_metadata", + statistic_id, + unit_of_measurement, + }); + +export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) => + hass.callWS({ + type: "recorder/clear_statistics", + statistic_ids, + }); + +export const calculateStatisticSumGrowth = ( + values: StatisticValue[] +): number | null => { + if (!values || values.length < 2) { + return null; + } + const endSum = values[values.length - 1].sum; + if (endSum === null) { + return null; + } + const startSum = values[0].sum; + if (startSum === null) { + return endSum; + } + return endSum - startSum; +}; + +export const calculateStatisticsSumGrowth = ( + data: Statistics, + stats: string[] +): number | null => { + let totalGrowth: number | null = null; + + for (const stat of stats) { + if (!(stat in data)) { + continue; + } + const statGrowth = calculateStatisticSumGrowth(data[stat]); + + if (statGrowth === null) { + continue; + } + if (totalGrowth === null) { + totalGrowth = statGrowth; + } else { + totalGrowth += statGrowth; + } + } + + return totalGrowth; +}; + +export const statisticsHaveType = ( + stats: StatisticValue[], + type: StatisticType +) => stats.some((stat) => stat[type] !== null); + +export const adjustStatisticsSum = ( + hass: HomeAssistant, + statistic_id: string, + start_time: string, + adjustment: number +): Promise => + hass.callWS({ + type: "recorder/adjust_sum_statistics", + statistic_id, + start_time, + adjustment, + }); + +export const getStatisticLabel = ( + hass: HomeAssistant, + statisticsId: string, + statisticsMetaData: StatisticsMetaData | undefined +): string => { + const entity = hass.states[statisticsId]; + if (entity) { + return computeStateName(entity); + } + return statisticsMetaData?.name || statisticsId; +}; diff --git a/src/panels/config/energy/components/ha-energy-battery-settings.ts b/src/panels/config/energy/components/ha-energy-battery-settings.ts index 376ace7323..9a7b3c6ad1 100644 --- a/src/panels/config/energy/components/ha-energy-battery-settings.ts +++ b/src/panels/config/energy/components/ha-energy-battery-settings.ts @@ -16,7 +16,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, diff --git a/src/panels/config/energy/components/ha-energy-device-settings.ts b/src/panels/config/energy/components/ha-energy-device-settings.ts index bac990fbc8..1d03ed1326 100644 --- a/src/panels/config/energy/components/ha-energy-device-settings.ts +++ b/src/panels/config/energy/components/ha-energy-device-settings.ts @@ -15,7 +15,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, diff --git a/src/panels/config/energy/components/ha-energy-gas-settings.ts b/src/panels/config/energy/components/ha-energy-gas-settings.ts index 29b81e2a85..44f2d4b2ba 100644 --- a/src/panels/config/energy/components/ha-energy-gas-settings.ts +++ b/src/panels/config/energy/components/ha-energy-gas-settings.ts @@ -16,7 +16,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index 0e78a01b95..e4e85d79c4 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -30,7 +30,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow"; import { showAlertDialog, diff --git a/src/panels/config/energy/components/ha-energy-solar-settings.ts b/src/panels/config/energy/components/ha-energy-solar-settings.ts index 8c8aef7ac1..a29faaeb76 100644 --- a/src/panels/config/energy/components/ha-energy-solar-settings.ts +++ b/src/panels/config/energy/components/ha-energy-solar-settings.ts @@ -16,7 +16,7 @@ import { import { StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { showConfirmationDialog, showAlertDialog, diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index c6a2d5b150..d779876137 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -13,7 +13,7 @@ import { import { getStatisticMetadata, StatisticsMetaData, -} from "../../../data/history"; +} from "../../../data/recorder"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index cf0847bb05..980fabaa52 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -15,7 +15,7 @@ import { StatisticsMetaData, StatisticsValidationResult, validateStatistics, -} from "../../../data/history"; +} from "../../../data/recorder"; import { showAlertDialog, showConfirmationDialog, diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts index ded0fc2c82..b9c54f3f6a 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -24,7 +24,7 @@ import { adjustStatisticsSum, fetchStatistics, StatisticValue, -} from "../../../data/history"; +} from "../../../data/recorder"; import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle, haStyleDialog } from "../../../resources/styles"; diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts index 3168bffc9f..5d11ba98ed 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -8,7 +8,7 @@ import { HomeAssistant } from "../../../types"; import { clearStatistics, updateStatisticsMetadata, -} from "../../../data/history"; +} from "../../../data/recorder"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts index 4bfaebe489..0058fa49d2 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-fix-unsupported-unit-meta.ts @@ -5,7 +5,7 @@ import "../../../components/ha-dialog"; import { fireEvent } from "../../../common/dom/fire_event"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { updateStatisticsMetadata } from "../../../data/history"; +import { updateStatisticsMetadata } from "../../../data/recorder"; import "../../../components/ha-formfield"; import "../../../components/ha-radio"; import type { DialogStatisticsUnsupportedUnitMetaParams } from "./show-dialog-statistics-fix-unsupported-unit-meta"; diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts index 1db2c76307..6248a6c42f 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsMetaData } from "../../../data/history"; +import { StatisticsMetaData } from "../../../data/recorder"; export const loadAdjustSumDialog = () => import("./dialog-statistics-adjust-sum"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts index c341bfdf21..1b6d7ee116 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsValidationResultUnitsChanged } from "../../../data/history"; +import { StatisticsValidationResultUnitsChanged } from "../../../data/recorder"; export const loadFixUnitsDialog = () => import("./dialog-statistics-fix-units-changed"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts index 828f3094bc..8ea4c5d915 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts +++ b/src/panels/developer-tools/statistics/show-dialog-statistics-fix-unsupported-unit-meta.ts @@ -1,5 +1,5 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { StatisticsValidationResultUnsupportedUnitMetadata } from "../../../data/history"; +import { StatisticsValidationResultUnsupportedUnitMetadata } from "../../../data/recorder"; export const loadFixUnsupportedUnitMetaDialog = () => import("./dialog-statistics-fix-unsupported-unit-meta"); diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index ea2c81d949..20dce6b0f6 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -13,7 +13,7 @@ import { energySourcesByType, getEnergyDataCollection, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import { createEntityNotFoundWarning } from "../../components/hui-warning"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index a1419466c5..10cab6c299 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -27,7 +27,7 @@ import { fetchStatistics, getStatisticLabel, Statistics, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index d42863015f..d204562559 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -24,7 +24,7 @@ import { getEnergyDataCollection, getEnergyGasUnit, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts index 6ec689b4f8..fc940d5605 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-gas-graph-card.ts @@ -42,7 +42,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts index 9e6ad9edec..1b3897b7d1 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -13,7 +13,7 @@ import { getEnergyDataCollection, GridSourceTypeEnergyPreference, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index c7c8f3c829..5e5df30f85 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -12,7 +12,7 @@ import { energySourcesByType, getEnergyDataCollection, } from "../../../../data/energy"; -import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 5b29e94c53..ac985edef9 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -43,7 +43,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index ec1c20ee45..64779926c3 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -30,7 +30,7 @@ import { import { calculateStatisticSumGrowth, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index 1e315bcc37..ebae24dfca 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -37,7 +37,7 @@ import { Statistics, StatisticsMetaData, getStatisticLabel, -} from "../../../../data/history"; +} from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 370857f9f0..4a6f4d39c7 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -15,7 +15,7 @@ import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; import { LovelaceCard } from "../types"; import { StatisticsGraphCardConfig } from "./types"; -import { fetchStatistics, Statistics } from "../../../data/history"; +import { fetchStatistics, Statistics } from "../../../data/recorder"; @customElement("hui-statistics-graph-card") export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ffd0cc3555..7113ad1d13 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -1,4 +1,4 @@ -import { StatisticType } from "../../../data/history"; +import { StatisticType } from "../../../data/recorder"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { FullCalendarView, TranslationDict } from "../../../types"; import { Condition } from "../common/validate-condition"; From 782c95cf041efa1fa1bcc67b7fe7795abb3919df Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 12 Sep 2022 15:53:18 +0200 Subject: [PATCH 09/98] Fix empty value for state picker (#13699) --- .../entity/ha-entity-state-picker.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/entity/ha-entity-state-picker.ts b/src/components/entity/ha-entity-state-picker.ts index 21f83ae948..98b5640b67 100644 --- a/src/components/entity/ha-entity-state-picker.ts +++ b/src/components/entity/ha-entity-state-picker.ts @@ -8,6 +8,7 @@ import { HomeAssistant } from "../../types"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import { formatAttributeValue } from "../../data/entity_attributes"; +import { fireEvent } from "../../common/dom/fire_event"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; @@ -70,7 +71,7 @@ class HaEntityStatePicker extends LitElement { return html` ) { this._opened = ev.detail.value; } private _valueChanged(ev: PolymerChangedEvent) { - this.value = ev.detail.value; + ev.stopPropagation(); + const newValue = ev.detail.value; + if (newValue !== this._value) { + this._setValue(newValue); + } + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); + }, 0); } } From 9ea1f61971dd5667350c3a1f52233bc6f34c964f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 12 Sep 2022 15:55:03 +0200 Subject: [PATCH 10/98] Display entity friendly name for disabled entities (#13696) --- src/data/entity_registry.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 81a430dce4..78da19f6f2 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -93,7 +93,10 @@ export const computeEntityRegistryName = ( return entry.name; } const state = hass.states[entry.entity_id]; - return state ? computeStateName(state) : entry.entity_id; + if (state) { + return computeStateName(state); + } + return entry.original_name ? entry.original_name : entry.entity_id; }; export const getExtendedEntityRegistryEntry = ( From 6902537666b5094a46ea533f35803c4dac9dc769 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 12 Sep 2022 18:04:07 +0200 Subject: [PATCH 11/98] Fix script config editor (#13703) --- src/panels/config/script/ha-script-editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 2eea1afc6f..004547ea28 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -609,13 +609,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { switch (key) { case "id": this._idChanged(value); - return; + break; case "alias": this._aliasChanged(value); break; case "mode": this._modeChanged(value); - return; + break; } if (values[key] === undefined) { From d70cb2472297f067afc26ede7280205d050c094e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 13 Sep 2022 10:40:18 +0200 Subject: [PATCH 12/98] Revert "Either show range or fix target temperature... #13638 (#13706) --- src/dialogs/more-info/controls/more-info-climate.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 1715a94703..2a66080055 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -98,9 +98,7 @@ class MoreInfoClimate extends LitElement {
    ` : ""} - ${supportTargetTemperature && - !supportTargetTemperatureRange && - stateObj.attributes.temperature !== undefined && + ${stateObj.attributes.temperature !== undefined && stateObj.attributes.temperature !== null ? html` ` : ""} - ${supportTargetTemperatureRange && - ((stateObj.attributes.target_temp_low !== undefined && + ${(stateObj.attributes.target_temp_low !== undefined && stateObj.attributes.target_temp_low !== null) || - (stateObj.attributes.target_temp_high !== undefined && - stateObj.attributes.target_temp_high !== null)) + (stateObj.attributes.target_temp_high !== undefined && + stateObj.attributes.target_temp_high !== null) ? html` Date: Tue, 13 Sep 2022 10:40:48 +0200 Subject: [PATCH 13/98] Fix customizing sensor unit (#13710) --- src/panels/config/entities/entity-registry-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index da15534590..8ae1e84046 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -900,7 +900,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { params.hidden_by = this._hiddenBy; } if ( - (domain === "number" || domain === "number") && + (domain === "number" || domain === "sensor") && stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement ) { params.options_domain = domain; From 4f3c708109769003143d6a9dcf43cbd650345b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 13 Sep 2022 10:41:19 +0200 Subject: [PATCH 14/98] Guard repairs subscription (#13708) --- src/components/ha-sidebar.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 3425cb35d4..30c4959335 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -221,13 +221,15 @@ class HaSidebar extends SubscribeMixin(LitElement) { private _sortable?: SortableInstance; public hassSubscribe(): UnsubscribeFunc[] { - return [ - subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { - this._issuesCount = repairs.issues.filter( - (issue) => !issue.ignored - ).length; - }), - ]; + return this.hass.user?.is_admin + ? [ + subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => { + this._issuesCount = repairs.issues.filter( + (issue) => !issue.ignored + ).length; + }), + ] + : []; } protected render() { From 771733d326346624236dcdd4e79d849b5be91ec2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 13 Sep 2022 10:42:15 +0200 Subject: [PATCH 15/98] Fix rtl support for hass subpage layout (#13702) --- src/layouts/hass-subpage.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 10859151c8..2dd0979543 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -1,6 +1,15 @@ -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, eventOptions, property } from "lit/decorators"; import { restoreScroll } from "../common/decorators/restore-scroll"; +import { toggleAttribute } from "../common/dom/toggle_attribute"; +import { computeRTL } from "../common/util/compute_rtl"; import "../components/ha-icon-button-arrow-prev"; import "../components/ha-menu-button"; import { HomeAssistant } from "../types"; @@ -24,6 +33,14 @@ class HassSubpage extends LitElement { // @ts-ignore @restoreScroll(".content") private _savedScrollPos?: number; + protected willUpdate(changedProps: PropertyValues): void { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if (!oldHass || oldHass.locale !== this.hass.locale) { + toggleAttribute(this, "rtl", computeRTL(this.hass)); + } + super.willUpdate(changedProps); + } + protected render(): TemplateResult { return html`
    From 7696df56ac64e2957a424c8b8e8695b769a8a2ee Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 13 Sep 2022 10:43:38 +0200 Subject: [PATCH 16/98] Don't use selector inside choose action (#13705) --- .../action/types/ha-automation-action-choose.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts index 67a70e4304..4ab8dd1538 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts @@ -9,7 +9,6 @@ import { Action, ChooseAction } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; import { ActionElement } from "../ha-automation-action-row"; -import "../../../../../components/ha-form/ha-form"; @customElement("ha-automation-action-choose") export class HaChooseAction extends LitElement implements ActionElement { @@ -64,13 +63,13 @@ export class HaChooseAction extends LitElement implements ActionElement { "ui.panel.config.automation.editor.actions.type.choose.sequence" )}: - + >
    ` )} @@ -126,7 +125,7 @@ export class HaChooseAction extends LitElement implements ActionElement { private _actionChanged(ev: CustomEvent) { ev.stopPropagation(); - const value = ev.detail.value.sequence as Action[]; + const value = ev.detail.value as Action[]; const index = (ev.target as any).idx; const choose = this.action.choose ? [...ensureArray(this.action.choose)] @@ -185,9 +184,6 @@ export class HaChooseAction extends LitElement implements ActionElement { right: 0; padding: 4px; } - ha-form::part(root) { - overflow: visible; - } ha-svg-icon { height: 20px; } From c95d19299b816ebe26fdc52b2f73767495827092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 13 Sep 2022 19:58:32 +0200 Subject: [PATCH 17/98] Fix open config flow when coming from a my link (#13720) --- .../integrations/ha-config-integrations.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index 3ec709d2c5..daee2bee47 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -695,22 +695,22 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { if (handlers.includes(domain)) { const localize = await localizePromise; if ( - !(await showConfirmationDialog(this, { + await showConfirmationDialog(this, { title: localize("ui.panel.config.integrations.confirm_new", { integration: domainToName(localize, domain), }), - })) + }) ) { - return; + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._handleFlowUpdated(); + }, + startFlowHandler: domain, + manifest: this._manifests[domain], + showAdvanced: this.hass.userData?.showAdvanced, + }); } - showConfigFlowDialog(this, { - dialogClosedCallback: () => { - this._handleFlowUpdated(); - }, - startFlowHandler: domain, - manifest: this._manifests[domain], - showAdvanced: this.hass.userData?.showAdvanced, - }); + return; } const supportedBrands = await getSupportedBrands(this.hass); From f4dc74b2e8d1bcc9659ed667e58ca11ea85baef3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 13 Sep 2022 19:59:35 +0200 Subject: [PATCH 18/98] Show confirm dialog when clicking traces (#13716) --- .../config/automation/ha-automation-editor.ts | 72 ++++++++---------- src/panels/config/script/ha-script-editor.ts | 73 ++++++++----------- 2 files changed, 63 insertions(+), 82 deletions(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index f61b1f6438..14f34c7f6b 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -31,7 +31,6 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; -import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -135,17 +134,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { > ${this._config?.id && !this.narrow ? html` - - - ${this.hass.localize( - "ui.panel.config.automation.editor.show_trace" - )} - - + + ${this.hass.localize( + "ui.panel.config.automation.editor.show_trace" + )} + ` : ""} @@ -486,6 +479,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { fireEvent(this, "hass-more-info", { entityId: this._entityId }); } + private async _showTrace() { + if (this._config?.id) { + const result = await this.confirmUnsavedChanged(); + if (result) { + navigate(`/config/automation/trace/${this._config.id}`); + } + } + } + private _runActions() { if (!this.hass || !this._entityId) { return; @@ -535,44 +537,35 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { this._dirty = true; } - private _backTapped = (): void => { + private async confirmUnsavedChanged(): Promise { if (this._dirty) { - showConfirmationDialog(this, { + return showConfirmationDialog(this, { text: this.hass!.localize( "ui.panel.config.automation.editor.unsaved_confirm" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), - confirm: () => { - afterNextRender(() => history.back()); - }, }); - } else { - afterNextRender(() => history.back()); + } + return true; + } + + private _backTapped = async () => { + const result = await this.confirmUnsavedChanged(); + if (result) { + history.back(); } }; private async _duplicate() { - if (this._dirty) { - if ( - !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm" - ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - })) - ) { - return; - } - // Wait for dialog to complete closing - await new Promise((resolve) => setTimeout(resolve, 0)); + const result = await this.confirmUnsavedChanged(); + if (result) { + showAutomationEditor({ + ...this._config, + id: undefined, + alias: undefined, + }); } - showAutomationEditor({ - ...this._config, - id: undefined, - alias: undefined, - }); } private async _deleteConfirm() { @@ -689,9 +682,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { flex-direction: column; padding-bottom: 0; } - .trace-link { - text-decoration: none; - } manual-automation-editor, blueprint-automation-editor { margin: 0 auto; diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 004547ea28..d39e81a473 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -184,17 +184,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > ${this.scriptEntityId && !this.narrow ? html` - - - ${this.hass.localize( - "ui.panel.config.script.editor.show_trace" - )} - - + + ${this.hass.localize( + "ui.panel.config.script.editor.show_trace" + )} + ` : ""} @@ -530,6 +524,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); } + private async _showTrace() { + if (this.scriptEntityId) { + const result = await this.confirmUnsavedChanged(); + if (result) { + navigate(`/config/script/trace/${this.scriptEntityId}`); + } + } + } + private async _runScript(ev: CustomEvent) { ev.stopPropagation(); await triggerScript(this.hass, this.scriptEntityId as string); @@ -660,45 +663,36 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this._dirty = true; } - private _backTapped = (): void => { + private async confirmUnsavedChanged(): Promise { if (this._dirty) { - showConfirmationDialog(this, { + return showConfirmationDialog(this, { text: this.hass!.localize( - "ui.panel.config.common.editor.confirm_unsaved" + "ui.panel.config.automation.editor.unsaved_confirm" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), - confirm: () => { - setTimeout(() => history.back()); - }, }); - } else { + } + return true; + } + + private _backTapped = async () => { + const result = await this.confirmUnsavedChanged(); + if (result) { history.back(); } }; private async _duplicate() { - if (this._dirty) { - if ( - !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.common.editor.confirm_unsaved" - ), - confirmText: this.hass!.localize("ui.common.yes"), - dismissText: this.hass!.localize("ui.common.no"), - })) - ) { - return; - } - // Wait for dialog to complete closing - await new Promise((resolve) => setTimeout(resolve, 0)); + const result = await this.confirmUnsavedChanged(); + if (result) { + showScriptEditor({ + ...this._config, + alias: `${this._config?.alias} (${this.hass.localize( + "ui.panel.config.script.picker.duplicate" + )})`, + }); } - showScriptEditor({ - ...this._config, - alias: `${this._config?.alias} (${this.hass.localize( - "ui.panel.config.script.picker.duplicate" - )})`, - }); } private async _deleteConfirm() { @@ -840,9 +834,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { .header a { color: var(--secondary-text-color); } - .trace-link { - text-decoration: none; - } ha-button-menu a { text-decoration: none; color: var(--primary-color); From b84240edbc4eb25f5d574561840c5ef5ec403467 Mon Sep 17 00:00:00 2001 From: Pierre <397503+bemble@users.noreply.github.com> Date: Tue, 13 Sep 2022 20:00:20 +0200 Subject: [PATCH 19/98] fix: add previous repeat action configuration on change (#13717) --- .../automation/action/types/ha-automation-action-repeat.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts index c7dc7c8eab..f0994f6925 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts @@ -115,6 +115,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { fireEvent(this, "value-changed", { value: { + ...this.action, repeat: { [type]: value, sequence: this.action.repeat.sequence }, }, }); @@ -125,6 +126,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { const value = ev.detail.value as Condition[]; fireEvent(this, "value-changed", { value: { + ...this.action, repeat: { ...this.action.repeat, [getType(this.action.repeat)!]: value, @@ -138,6 +140,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { const value = ev.detail.value as Action[]; fireEvent(this, "value-changed", { value: { + ...this.action, repeat: { ...this.action.repeat, sequence: value, @@ -153,6 +156,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { } fireEvent(this, "value-changed", { value: { + ...this.action, repeat: { ...this.action.repeat, count: newVal, From 5d4c090b26e0540ab88334b7b251fb15f13fe5a3 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 14 Sep 2022 02:15:01 +0800 Subject: [PATCH 20/98] Bump hls.js to v1.2.3 (#13718) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d27157841f..987792155e 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "deep-freeze": "^0.0.1", "fuse.js": "^6.0.0", "google-timezones-json": "^1.0.2", - "hls.js": "^1.2.1", + "hls.js": "^1.2.3", "home-assistant-js-websocket": "^8.0.0", "idb-keyval": "^5.1.3", "intl-messageformat": "^9.9.1", diff --git a/yarn.lock b/yarn.lock index 40f398ce50..6c3e5251be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8940,10 +8940,10 @@ fsevents@^1.2.7: languageName: node linkType: hard -"hls.js@npm:^1.2.1": - version: 1.2.1 - resolution: "hls.js@npm:1.2.1" - checksum: 19dd63a0bae5ca5e58a12cf0d4d7da353cdb88bb2ffbc691edcd0ceca7e57ca6ab4189bb55b326b16ed8c12d0053d94ea14eb1617e4279fc73cee11d33bf7f90 +"hls.js@npm:^1.2.3": + version: 1.2.3 + resolution: "hls.js@npm:1.2.3" + checksum: 9d07957d94e84b8f187aa209ffb0e6f65302178c6fdebc6effd63644d8cf2e304b2232109a30336cfee02c908aba86a402fe5d5dfa6487aca3387fd5748cccfa languageName: node linkType: hard @@ -9095,7 +9095,7 @@ fsevents@^1.2.7: gulp-merge-json: ^1.3.1 gulp-rename: ^2.0.0 gulp-zopfli-green: ^3.0.1 - hls.js: ^1.2.1 + hls.js: ^1.2.3 home-assistant-js-websocket: ^8.0.0 html-minifier: ^4.0.0 husky: ^8.0.1 From 23e5a47b3be5897458337f5c9fb18ea7cadfb5c0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 13 Sep 2022 20:49:39 +0200 Subject: [PATCH 21/98] improve duration rendering for state trigger (#13723) --- src/data/automation_i18n.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 93a6970cc9..e81e6c992a 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -1,3 +1,4 @@ +import { formatDuration } from "../common/datetime/format_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration"; import { ensureArray } from "../common/ensure-array"; import { computeStateName } from "../common/entity/compute_state_name"; @@ -140,17 +141,19 @@ export const describeTrigger = ( base += ` to ${to}`; } - if ("for" in trigger) { - let duration: string; + if (trigger.for) { + let duration: string | null; if (typeof trigger.for === "number") { - duration = `${secondsToDuration(trigger.for)!}`; + duration = secondsToDuration(trigger.for); } else if (typeof trigger.for === "string") { - duration = `${trigger.for}`; + duration = trigger.for; } else { - duration = `${JSON.stringify(trigger.for)}`; + duration = formatDuration(trigger.for); } - base += ` for ${duration}`; + if (duration) { + base += ` for ${duration}`; + } } return base; From b3d64fc52aa24460aa5c85d34ec3a90940e069a5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 13 Sep 2022 20:53:19 +0200 Subject: [PATCH 22/98] use the correct inputmode (#13722) --- src/components/ha-selector/ha-selector-number.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 0ccfedafd2..343490af31 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -51,8 +51,9 @@ export class HaNumberSelector extends LitElement { ` : ""} Date: Tue, 13 Sep 2022 20:53:55 +0200 Subject: [PATCH 23/98] Revert "Add initial field to the helper input_number in UI" (#13713) --- .../config/helpers/forms/ha-input_number-form.ts | 13 ------------- src/translations/en.json | 1 - 2 files changed, 14 deletions(-) diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index e6de46796f..a1196cbf22 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -26,8 +26,6 @@ class HaInputNumberForm extends LitElement { @state() private _min?: number; - @state() private _initial?: number; - @state() private _mode?: string; @state() private _step?: number; @@ -44,7 +42,6 @@ class HaInputNumberForm extends LitElement { this._min = item.min ?? 0; this._mode = item.mode || "slider"; this._step = item.step ?? 1; - this._initial = item.initial ?? 0; this._unit_of_measurement = item.unit_of_measurement; } else { this._item = { @@ -57,7 +54,6 @@ class HaInputNumberForm extends LitElement { this._min = 0; this._mode = "slider"; this._step = 1; - this._initial = 0; } } @@ -117,15 +113,6 @@ class HaInputNumberForm extends LitElement { "ui.dialogs.helper_settings.input_number.max" )} > - ${this.hass.userData?.showAdvanced ? html`
    diff --git a/src/translations/en.json b/src/translations/en.json index 4ca9e25056..c3fa44cf73 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -924,7 +924,6 @@ "pattern": "Regex pattern for client-side validation" }, "input_number": { - "initial": "Initial value", "min": "Minimum value", "max": "Maximum value", "mode": "Display mode", From 3c62bc9b187572247c7e14823684c044e3d41fd6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 14 Sep 2022 09:02:51 +0200 Subject: [PATCH 24/98] Fix hass subpage (#13730) --- src/layouts/hass-subpage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 2dd0979543..76322b6691 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -34,11 +34,14 @@ class HassSubpage extends LitElement { @restoreScroll(".content") private _savedScrollPos?: number; protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (!changedProps.has("hass")) { + return; + } const oldHass = changedProps.get("hass") as HomeAssistant | undefined; if (!oldHass || oldHass.locale !== this.hass.locale) { toggleAttribute(this, "rtl", computeRTL(this.hass)); } - super.willUpdate(changedProps); } protected render(): TemplateResult { From 490d46396eca6593f57989b05060bfc4ff770bbd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 14 Sep 2022 09:08:37 +0200 Subject: [PATCH 25/98] Fix system blank page (#13731) --- src/panels/config/core/ha-config-system-navigation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index e69948c657..42c71b95c5 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -116,6 +116,7 @@ class HaConfigSystemNavigation extends LitElement { return html` From 321914d53a2b8e3c5f7e0c56e7e72fd54d00c9a2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 14 Sep 2022 11:13:32 +0200 Subject: [PATCH 26/98] Fix empty hardware when fetching hardware info (#13738) --- src/panels/config/core/ha-config-system-navigation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index 42c71b95c5..2ac869d2d5 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -205,7 +205,7 @@ class HaConfigSystemNavigation extends LitElement { const hardwareInfo: HardwareInfo = await this.hass.callWS({ type: "hardware/info", }); - this._boardName = hardwareInfo?.hardware?.[0].name; + this._boardName = hardwareInfo?.hardware?.[0]?.name; } else if (isHassioLoaded) { const osData: HassioHassOSInfo = await fetchHassioHassOsInfo(this.hass); if (osData.board) { From 8c03bbdccc08144828298197d2b90c12ad39e923 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 15 Sep 2022 11:58:36 +0200 Subject: [PATCH 27/98] Improve light entity definition (#13744) --- gallery/src/pages/more-info/light.ts | 91 +++++++++++-------- src/data/light.ts | 77 +++++++++------- .../more-info/controls/more-info-light.ts | 60 ++++++------ src/panels/lovelace/cards/hui-light-card.ts | 11 ++- 4 files changed, 130 insertions(+), 109 deletions(-) diff --git a/gallery/src/pages/more-info/light.ts b/gallery/src/pages/more-info/light.ts index 999a4908ad..336aeb8ca8 100644 --- a/gallery/src/pages/more-info/light.ts +++ b/gallery/src/pages/more-info/light.ts @@ -1,12 +1,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../../../src/components/ha-card"; -import { - LightColorModes, - SUPPORT_EFFECT, - SUPPORT_FLASH, - SUPPORT_TRANSITION, -} from "../../../../src/data/light"; +import { LightColorMode, LightEntityFeature } from "../../../../src/data/light"; import "../../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../../src/fake_data/entity"; import { @@ -22,8 +17,8 @@ const ENTITIES = [ getEntity("light", "kitchen_light", "on", { friendly_name: "Brightness Light", brightness: 200, - supported_color_modes: [LightColorModes.BRIGHTNESS], - color_mode: LightColorModes.BRIGHTNESS, + supported_color_modes: [LightColorMode.BRIGHTNESS], + color_mode: LightColorMode.BRIGHTNESS, }), getEntity("light", "color_temperature_light", "on", { friendly_name: "White Color Temperature Light", @@ -32,10 +27,10 @@ const ENTITIES = [ min_mireds: 30, max_mireds: 150, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, ], - color_mode: LightColorModes.COLOR_TEMP, + color_mode: LightColorMode.COLOR_TEMP, }), getEntity("light", "color_hs_light", "on", { friendly_name: "Color HS Light", @@ -44,13 +39,16 @@ const ENTITIES = [ rgb_color: [30, 100, 255], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.HS, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.HS, ], - color_mode: LightColorModes.HS, + color_mode: LightColorMode.HS, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgb_ct_light", "on", { @@ -59,22 +57,28 @@ const ENTITIES = [ color_temp: 75, min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGB, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGB, ], - color_mode: LightColorModes.COLOR_TEMP, + color_mode: LightColorMode.COLOR_TEMP, effect_list: ["random", "colorloop"], }), getEntity("light", "color_RGB_light", "on", { friendly_name: "Color Effects Light", brightness: 255, rgb_color: [30, 100, 255], - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, - supported_color_modes: [LightColorModes.BRIGHTNESS, LightColorModes.RGB], - color_mode: LightColorModes.RGB, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, + supported_color_modes: [LightColorMode.BRIGHTNESS, LightColorMode.RGB], + color_mode: LightColorMode.RGB, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgbw_light", "on", { @@ -83,13 +87,16 @@ const ENTITIES = [ rgbw_color: [30, 100, 255, 125], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGBW, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGBW, ], - color_mode: LightColorModes.RGBW, + color_mode: LightColorMode.RGBW, effect_list: ["random", "colorloop"], }), getEntity("light", "color_rgbww_light", "on", { @@ -98,13 +105,16 @@ const ENTITIES = [ rgbww_color: [30, 100, 255, 125, 10], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.RGBWW, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.RGBWW, ], - color_mode: LightColorModes.RGBWW, + color_mode: LightColorMode.RGBWW, effect_list: ["random", "colorloop"], }), getEntity("light", "color_xy_light", "on", { @@ -114,13 +124,16 @@ const ENTITIES = [ rgb_color: [30, 100, 255], min_mireds: 30, max_mireds: 150, - supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION, + supported_features: + LightEntityFeature.EFFECT + + LightEntityFeature.FLASH + + LightEntityFeature.TRANSITION, supported_color_modes: [ - LightColorModes.BRIGHTNESS, - LightColorModes.COLOR_TEMP, - LightColorModes.XY, + LightColorMode.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.XY, ], - color_mode: LightColorModes.XY, + color_mode: LightColorMode.XY, effect_list: ["random", "colorloop"], }), ]; diff --git a/src/data/light.ts b/src/data/light.ts index 93ae0c1d75..a171568451 100644 --- a/src/data/light.ts +++ b/src/data/light.ts @@ -3,76 +3,83 @@ import { HassEntityBase, } from "home-assistant-js-websocket"; -export const enum LightColorModes { +export const enum LightEntityFeature { + EFFECT = 4, + FLASH = 8, + TRANSITION = 32, +} + +export const enum LightColorMode { UNKNOWN = "unknown", ONOFF = "onoff", BRIGHTNESS = "brightness", COLOR_TEMP = "color_temp", - WHITE = "white", HS = "hs", XY = "xy", RGB = "rgb", RGBW = "rgbw", RGBWW = "rgbww", + WHITE = "white", } const modesSupportingColor = [ - LightColorModes.HS, - LightColorModes.XY, - LightColorModes.RGB, - LightColorModes.RGBW, - LightColorModes.RGBWW, + LightColorMode.HS, + LightColorMode.XY, + LightColorMode.RGB, + LightColorMode.RGBW, + LightColorMode.RGBWW, ]; -const modesSupportingDimming = [ +const modesSupportingBrightness = [ ...modesSupportingColor, - LightColorModes.COLOR_TEMP, - LightColorModes.BRIGHTNESS, + LightColorMode.COLOR_TEMP, + LightColorMode.BRIGHTNESS, + LightColorMode.WHITE, ]; -export const SUPPORT_EFFECT = 4; -export const SUPPORT_FLASH = 8; -export const SUPPORT_TRANSITION = 32; - export const lightSupportsColorMode = ( entity: LightEntity, - mode: LightColorModes -) => entity.attributes.supported_color_modes?.includes(mode); + mode: LightColorMode +) => entity.attributes.supported_color_modes?.includes(mode) || false; export const lightIsInColorMode = (entity: LightEntity) => - modesSupportingColor.includes(entity.attributes.color_mode); + (entity.attributes.color_mode && + modesSupportingColor.includes(entity.attributes.color_mode)) || + false; export const lightSupportsColor = (entity: LightEntity) => entity.attributes.supported_color_modes?.some((mode) => modesSupportingColor.includes(mode) ); -export const lightSupportsDimming = (entity: LightEntity) => +export const lightSupportsBrightness = (entity: LightEntity) => entity.attributes.supported_color_modes?.some((mode) => - modesSupportingDimming.includes(mode) - ); + modesSupportingBrightness.includes(mode) + ) || false; -export const getLightCurrentModeRgbColor = (entity: LightEntity): number[] => - entity.attributes.color_mode === LightColorModes.RGBWW +export const getLightCurrentModeRgbColor = ( + entity: LightEntity +): number[] | undefined => + entity.attributes.color_mode === LightColorMode.RGBWW ? entity.attributes.rgbww_color - : entity.attributes.color_mode === LightColorModes.RGBW + : entity.attributes.color_mode === LightColorMode.RGBW ? entity.attributes.rgbw_color : entity.attributes.rgb_color; interface LightEntityAttributes extends HassEntityAttributeBase { - min_mireds: number; - max_mireds: number; - friendly_name: string; - brightness: number; - hs_color: [number, number]; - rgb_color: [number, number, number]; - rgbw_color: [number, number, number, number]; - rgbww_color: [number, number, number, number, number]; - color_temp: number; + min_mireds?: number; + max_mireds?: number; + brightness?: number; + xy_color?: [number, number]; + hs_color?: [number, number]; + color_temp?: number; + rgb_color?: [number, number, number]; + rgbw_color?: [number, number, number, number]; + rgbww_color?: [number, number, number, number, number]; effect?: string; - effect_list: string[] | null; - supported_color_modes: LightColorModes[]; - color_mode: LightColorModes; + effect_list?: string[] | null; + supported_color_modes?: LightColorMode[]; + color_mode?: LightColorMode; } export interface LightEntity extends HassEntityBase { diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 29fb2644d8..be85eb7921 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -20,13 +20,13 @@ import "../../../components/ha-labeled-slider"; import "../../../components/ha-select"; import { getLightCurrentModeRgbColor, - LightColorModes, + LightColorMode, LightEntity, + LightEntityFeature, lightIsInColorMode, lightSupportsColor, lightSupportsColorMode, - lightSupportsDimming, - SUPPORT_EFFECT, + lightSupportsBrightness, } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; @@ -56,7 +56,7 @@ class MoreInfoLight extends LitElement { @state() private _colorPickerColor?: [number, number, number]; - @state() private _mode?: "color" | LightColorModes; + @state() private _mode?: "color" | LightColorMode; protected render(): TemplateResult { if (!this.hass || !this.stateObj) { @@ -65,29 +65,29 @@ class MoreInfoLight extends LitElement { const supportsTemp = lightSupportsColorMode( this.stateObj, - LightColorModes.COLOR_TEMP + LightColorMode.COLOR_TEMP ); const supportsWhite = lightSupportsColorMode( this.stateObj, - LightColorModes.WHITE + LightColorMode.WHITE ); const supportsRgbww = lightSupportsColorMode( this.stateObj, - LightColorModes.RGBWW + LightColorMode.RGBWW ); const supportsRgbw = !supportsRgbww && - lightSupportsColorMode(this.stateObj, LightColorModes.RGBW); + lightSupportsColorMode(this.stateObj, LightColorMode.RGBW); const supportsColor = supportsRgbww || supportsRgbw || lightSupportsColor(this.stateObj); return html`
    - ${lightSupportsDimming(this.stateObj) + ${lightSupportsBrightness(this.stateObj) ? html` @@ -260,31 +260,31 @@ class MoreInfoLight extends LitElement { let brightnessAdjust = 100; this._brightnessAdjusted = undefined; if ( - stateObj.attributes.color_mode === LightColorModes.RGB && - !lightSupportsColorMode(stateObj, LightColorModes.RGBWW) && - !lightSupportsColorMode(stateObj, LightColorModes.RGBW) + stateObj.attributes.color_mode === LightColorMode.RGB && + !lightSupportsColorMode(stateObj, LightColorMode.RGBWW) && + !lightSupportsColorMode(stateObj, LightColorMode.RGBW) ) { - const maxVal = Math.max(...stateObj.attributes.rgb_color); + const maxVal = Math.max(...stateObj.attributes.rgb_color!); if (maxVal < 255) { this._brightnessAdjusted = maxVal; brightnessAdjust = (this._brightnessAdjusted / 255) * 100; } } this._brightnessSliderValue = Math.round( - (stateObj.attributes.brightness * brightnessAdjust) / 255 + ((stateObj.attributes.brightness || 0) * brightnessAdjust) / 255 ); this._ctSliderValue = stateObj.attributes.color_temp; this._wvSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBW - ? Math.round((stateObj.attributes.rgbw_color[3] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBW + ? Math.round((stateObj.attributes.rgbw_color![3] * 100) / 255) : undefined; this._cwSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBWW - ? Math.round((stateObj.attributes.rgbww_color[3] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![3] * 100) / 255) : undefined; this._wwSliderValue = - stateObj.attributes.color_mode === LightColorModes.RGBWW - ? Math.round((stateObj.attributes.rgbww_color[4] * 100) / 255) + stateObj.attributes.color_mode === LightColorMode.RGBWW + ? Math.round((stateObj.attributes.rgbww_color![4] * 100) / 255) : undefined; const currentRgbColor = getLightCurrentModeRgbColor(stateObj); @@ -307,10 +307,10 @@ class MoreInfoLight extends LitElement { (supportsTemp: boolean, supportsWhite: boolean) => { const modes = [{ label: "Color", value: "color" }]; if (supportsTemp) { - modes.push({ label: "Temperature", value: LightColorModes.COLOR_TEMP }); + modes.push({ label: "Temperature", value: LightColorMode.COLOR_TEMP }); } if (supportsWhite) { - modes.push({ label: "White", value: LightColorModes.WHITE }); + modes.push({ label: "White", value: LightColorMode.WHITE }); } return modes; } @@ -342,7 +342,7 @@ class MoreInfoLight extends LitElement { this._brightnessSliderValue = bri; - if (this._mode === LightColorModes.WHITE) { + if (this._mode === LightColorMode.WHITE) { this.hass.callService("light", "turn_on", { entity_id: this.stateObj!.entity_id, white: Math.min(255, Math.round((bri * 255) / 100)), @@ -486,7 +486,7 @@ class MoreInfoLight extends LitElement { } private _setRgbWColor(rgbColor: [number, number, number]) { - if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW)) { + if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW)) { const rgbww_color: [number, number, number, number, number] = this .stateObj!.attributes.rgbww_color ? [...this.stateObj!.attributes.rgbww_color] @@ -495,7 +495,7 @@ class MoreInfoLight extends LitElement { entity_id: this.stateObj!.entity_id, rgbww_color: rgbColor.concat(rgbww_color.slice(3)), }); - } else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW)) { + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW)) { const rgbw_color: [number, number, number, number] = this.stateObj! .attributes.rgbw_color ? [...this.stateObj!.attributes.rgbw_color] @@ -524,8 +524,8 @@ class MoreInfoLight extends LitElement { ]; if ( - lightSupportsColorMode(this.stateObj!, LightColorModes.RGBWW) || - lightSupportsColorMode(this.stateObj!, LightColorModes.RGBW) + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBWW) || + lightSupportsColorMode(this.stateObj!, LightColorMode.RGBW) ) { this._setRgbWColor( this._colorBrightnessSliderValue @@ -535,7 +535,7 @@ class MoreInfoLight extends LitElement { ) : [ev.detail.rgb.r, ev.detail.rgb.g, ev.detail.rgb.b] ); - } else if (lightSupportsColorMode(this.stateObj!, LightColorModes.RGB)) { + } else if (lightSupportsColorMode(this.stateObj!, LightColorMode.RGB)) { const rgb_color: [number, number, number] = [ ev.detail.rgb.r, ev.detail.rgb.g, diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 595c299785..6717feef65 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -19,7 +19,7 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-state-icon"; import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; -import { LightEntity, lightSupportsDimming } from "../../../data/light"; +import { LightEntity, lightSupportsBrightness } from "../../../data/light"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; @@ -93,8 +93,9 @@ export class HuiLightCard extends LitElement implements LovelaceCard { `; } - const brightness = - Math.round((stateObj.attributes.brightness / 255) * 100) || 0; + const brightness = Math.round( + ((stateObj.attributes.brightness || 0) / 255) * 100 + ); const name = this._config.name ?? computeStateName(stateObj); @@ -121,14 +122,14 @@ export class HuiLightCard extends LitElement implements LovelaceCard { @value-changing=${this._dragEvent} @value-changed=${this._setBrightness} style=${styleMap({ - visibility: lightSupportsDimming(stateObj) + visibility: lightSupportsBrightness(stateObj) ? "visible" : "hidden", })} > Date: Thu, 15 Sep 2022 12:00:00 +0200 Subject: [PATCH 28/98] Allow to disable a select option (#13618) --- gallery/src/pages/components/ha-selector.ts | 42 +++++++++++++++++++ src/components/ha-formfield.ts | 3 ++ .../ha-selector/ha-selector-select.ts | 15 ++++--- src/data/selector.ts | 1 + 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index dcc099b636..b794d15671 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -195,6 +195,48 @@ const SCHEMAS: { }, }, }, + select_disabled_list: { + name: "Select disabled option", + selector: { + select: { + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + ], + mode: "list", + }, + }, + }, + select_disabled_multiple: { + name: "Select disabled option", + selector: { + select: { + multiple: true, + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + ], + mode: "list", + }, + }, + }, + select_disabled: { + name: "Select disabled option", + selector: { + select: { + options: [ + { label: "Option 1", value: "Option 1" }, + { label: "Option 2", value: "Option 2" }, + { label: "Option 3", value: "Option 3", disabled: true }, + { label: "Option 4", value: "Option 4", disabled: true }, + { label: "Option 5", value: "Option 5", disabled: true }, + { label: "Option 6", value: "Option 6" }, + ], + }, + }, + }, select_custom: { name: "Select (Custom)", selector: { diff --git a/src/components/ha-formfield.ts b/src/components/ha-formfield.ts index 8a52c5ba5a..c536f7e820 100644 --- a/src/components/ha-formfield.ts +++ b/src/components/ha-formfield.ts @@ -13,6 +13,9 @@ export class HaFormfield extends FormfieldBase { switch (input.tagName) { case "HA-CHECKBOX": case "HA-RADIO": + if ((input as any).disabled) { + break; + } (input as any).checked = !(input as any).checked; fireEvent(input, "change"); break; diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index 71869b0e68..d051b5f125 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -13,6 +13,7 @@ import type { HaComboBox } from "../ha-combo-box"; import "../ha-formfield"; import "../ha-radio"; import "../ha-select"; +import "../ha-input-helper-text"; @customElement("ha-selector-select") export class HaSelectSelector extends LitElement { @@ -50,7 +51,7 @@ export class HaSelectSelector extends LitElement { @@ -69,7 +70,7 @@ export class HaSelectSelector extends LitElement { @@ -112,7 +113,9 @@ export class HaSelectSelector extends LitElement { .disabled=${this.disabled} .required=${this.required && !value.length} .value=${this._filter} - .items=${options.filter((item) => !this.value?.includes(item.value))} + .items=${options.filter( + (item) => !item.disabled && !this.value?.includes(item.value) + )} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} > @@ -136,7 +139,7 @@ export class HaSelectSelector extends LitElement { .helper=${this.helper} .disabled=${this.disabled} .required=${this.required} - .items=${options} + .items=${options.filter((item) => !item.disabled)} .value=${this.value} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} @@ -157,7 +160,9 @@ export class HaSelectSelector extends LitElement { > ${options.map( (item: SelectOption) => html` - ${item.label} + ${item.label} ` )} diff --git a/src/data/selector.ts b/src/data/selector.ts index 1620f1c306..9b1e570fb4 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -189,6 +189,7 @@ export interface ObjectSelector { export interface SelectOption { value: string; label: string; + disabled?: boolean; } export interface SelectSelector { From 81b21f874bed88aec9bab780c0afadd30420ed3b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 15 Sep 2022 12:31:36 +0200 Subject: [PATCH 29/98] Improve cover entity definition (#13750) --- gallery/src/pages/more-info/cover.ts | 121 +++++++++--------- src/common/entity/feature_class_names.ts | 6 +- src/components/ha-cover-controls.ts | 11 +- src/components/ha-cover-tilt-controls.ts | 20 ++- src/data/cover.ts | 64 +++------ .../more-info/controls/more-info-cover.ts | 25 +++- 6 files changed, 124 insertions(+), 123 deletions(-) diff --git a/gallery/src/pages/more-info/cover.ts b/gallery/src/pages/more-info/cover.ts index 6afa1a6ff9..65f9ba4a25 100644 --- a/gallery/src/pages/more-info/cover.ts +++ b/gallery/src/pages/more-info/cover.ts @@ -1,16 +1,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../../../src/components/ha-card"; -import { - SUPPORT_OPEN, - SUPPORT_STOP, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, - SUPPORT_OPEN_TILT, - SUPPORT_STOP_TILT, - SUPPORT_CLOSE_TILT, - SUPPORT_SET_TILT_POSITION, -} from "../../../../src/data/cover"; +import { CoverEntityFeature } from "../../../../src/data/cover"; import "../../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../../src/fake_data/entity"; import { @@ -22,113 +13,127 @@ import "../../components/demo-more-infos"; const ENTITIES = [ getEntity("cover", "position_buttons", "on", { friendly_name: "Position Buttons", - supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE, + supported_features: + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE, }), getEntity("cover", "position_slider_half", "on", { friendly_name: "Position Half-Open", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 50, }), getEntity("cover", "position_slider_open", "on", { friendly_name: "Position Open", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 100, }), getEntity("cover", "position_slider_closed", "on", { friendly_name: "Position Closed", supported_features: - SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION, current_position: 0, }), getEntity("cover", "tilt_buttons", "on", { friendly_name: "Tilt Buttons", supported_features: - SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, }), getEntity("cover", "tilt_slider_half", "on", { friendly_name: "Tilt Half-Open", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 50, }), getEntity("cover", "tilt_slider_open", "on", { friendly_name: "Tilt Open", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 100, }), getEntity("cover", "tilt_slider_closed", "on", { friendly_name: "Tilt Closed", supported_features: - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 0, }), getEntity("cover", "position_slider_tilt_slider", "on", { friendly_name: "Both Sliders", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_position: 30, current_tilt_position: 70, }), getEntity("cover", "position_tilt_slider", "on", { friendly_name: "Position & Tilt Slider", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_tilt_position: 70, }), getEntity("cover", "position_slider_tilt", "on", { friendly_name: "Position Slider & Tilt", supported_features: - SUPPORT_OPEN + - SUPPORT_STOP + - SUPPORT_CLOSE + - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT, + CoverEntityFeature.OPEN + + CoverEntityFeature.STOP + + CoverEntityFeature.CLOSE + + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, current_position: 30, }), getEntity("cover", "position_slider_only_tilt_slider", "on", { friendly_name: "Position Slider Only & Tilt Buttons", supported_features: - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT, + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT, current_position: 30, }), getEntity("cover", "position_slider_only_tilt", "on", { friendly_name: "Position Slider Only & Tilt", supported_features: - SUPPORT_SET_POSITION + - SUPPORT_OPEN_TILT + - SUPPORT_STOP_TILT + - SUPPORT_CLOSE_TILT + - SUPPORT_SET_TILT_POSITION, + CoverEntityFeature.SET_POSITION + + CoverEntityFeature.OPEN_TILT + + CoverEntityFeature.STOP_TILT + + CoverEntityFeature.CLOSE_TILT + + CoverEntityFeature.SET_TILT_POSITION, current_position: 30, current_tilt_position: 70, }), diff --git a/src/common/entity/feature_class_names.ts b/src/common/entity/feature_class_names.ts index aaa9220c28..7898e9d872 100644 --- a/src/common/entity/feature_class_names.ts +++ b/src/common/entity/feature_class_names.ts @@ -1,10 +1,14 @@ import { HassEntity } from "home-assistant-js-websocket"; import { supportsFeature } from "./supports-feature"; +export type FeatureClassNames = Partial< + Record +>; + // Expects classNames to be an object mapping feature-bit -> className export const featureClassNames = ( stateObj: HassEntity, - classNames: { [feature: number]: string } + classNames: FeatureClassNames ) => { if (!stateObj || !stateObj.attributes.supported_features) { return ""; diff --git a/src/components/ha-cover-controls.ts b/src/components/ha-cover-controls.ts index 73a1acbc5d..95b93ac039 100644 --- a/src/components/ha-cover-controls.ts +++ b/src/components/ha-cover-controls.ts @@ -3,15 +3,14 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon"; +import { supportsFeature } from "../common/entity/supports-feature"; import { CoverEntity, + CoverEntityFeature, isClosing, isFullyClosed, isFullyOpen, isOpening, - supportsClose, - supportsOpen, - supportsStop, } from "../data/cover"; import { UNAVAILABLE } from "../data/entity"; import type { HomeAssistant } from "../types"; @@ -32,7 +31,7 @@ class HaCoverControls extends LitElement {
    - supportsFeature(stateObj, SUPPORT_OPEN); - -export const supportsClose = (stateObj) => - supportsFeature(stateObj, SUPPORT_CLOSE); - -export const supportsSetPosition = (stateObj) => - supportsFeature(stateObj, SUPPORT_SET_POSITION); - -export const supportsStop = (stateObj) => - supportsFeature(stateObj, SUPPORT_STOP); - -export const supportsOpenTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_OPEN_TILT); - -export const supportsCloseTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_CLOSE_TILT); - -export const supportsStopTilt = (stateObj) => - supportsFeature(stateObj, SUPPORT_STOP_TILT); - -export const supportsSetTiltPosition = (stateObj) => - supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION); +export const enum CoverEntityFeature { + OPEN = 1, + CLOSE = 2, + SET_POSITION = 4, + STOP = 8, + OPEN_TILT = 16, + CLOSE_TILT = 32, + STOP_TILT = 64, + SET_TILT_POSITION = 128, +} export function isFullyOpen(stateObj: CoverEntity) { if (stateObj.attributes.current_position !== undefined) { @@ -77,17 +47,19 @@ export function isClosing(stateObj: CoverEntity) { export function isTiltOnly(stateObj: CoverEntity) { const supportsCover = - supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj); + supportsFeature(stateObj, CoverEntityFeature.OPEN) || + supportsFeature(stateObj, CoverEntityFeature.CLOSE) || + supportsFeature(stateObj, CoverEntityFeature.STOP); const supportsTilt = - supportsOpenTilt(stateObj) || - supportsCloseTilt(stateObj) || - supportsStopTilt(stateObj); + supportsFeature(stateObj, CoverEntityFeature.OPEN_TILT) || + supportsFeature(stateObj, CoverEntityFeature.CLOSE_TILT) || + supportsFeature(stateObj, CoverEntityFeature.STOP_TILT); return supportsTilt && !supportsCover; } interface CoverEntityAttributes extends HassEntityAttributeBase { - current_position: number; - current_tilt_position: number; + current_position?: number; + current_tilt_position?: number; } export interface CoverEntity extends HassEntityBase { diff --git a/src/dialogs/more-info/controls/more-info-cover.ts b/src/dialogs/more-info/controls/more-info-cover.ts index 61faf09af2..6f6125578b 100644 --- a/src/dialogs/more-info/controls/more-info-cover.ts +++ b/src/dialogs/more-info/controls/more-info-cover.ts @@ -1,19 +1,29 @@ import { css, CSSResult, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { attributeClassNames } from "../../../common/entity/attribute_class_names"; -import { featureClassNames } from "../../../common/entity/feature_class_names"; +import { + FeatureClassNames, + featureClassNames, +} from "../../../common/entity/feature_class_names"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attributes"; import "../../../components/ha-cover-tilt-controls"; import "../../../components/ha-labeled-slider"; import { CoverEntity, - FEATURE_CLASS_NAMES, + CoverEntityFeature, isTiltOnly, - supportsSetPosition, - supportsSetTiltPosition, } from "../../../data/cover"; import { HomeAssistant } from "../../../types"; +export const FEATURE_CLASS_NAMES: FeatureClassNames = { + [CoverEntityFeature.SET_POSITION]: "has-set_position", + [CoverEntityFeature.OPEN_TILT]: "has-open_tilt", + [CoverEntityFeature.CLOSE_TILT]: "has-close_tilt", + [CoverEntityFeature.STOP_TILT]: "has-stop_tilt", + [CoverEntityFeature.SET_TILT_POSITION]: "has-set_tilt_position", +}; + @customElement("more-info-cover") class MoreInfoCover extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -34,13 +44,16 @@ class MoreInfoCover extends LitElement { .caption=${this.hass.localize("ui.card.cover.position")} pin="" .value=${this.stateObj.attributes.current_position} - .disabled=${!supportsSetPosition(this.stateObj)} + .disabled=${!supportsFeature( + this.stateObj, + CoverEntityFeature.SET_POSITION + )} @change=${this._coverPositionSliderChanged} >
    - ${supportsSetTiltPosition(this.stateObj) + ${supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION) ? // Either render the labeled slider and put the tilt buttons into its slot // or (if tilt position is not supported and therefore no slider is shown) // render a title
    (same style as for a labeled slider) and directly put From 544c8fe3bb8c1750593e95c74aa7abd708d8fe8c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 Sep 2022 13:58:26 +0200 Subject: [PATCH 30/98] Improve `StatisticsChart` to only fetch needed metadata (#13617) --- src/components/chart/statistics-chart.ts | 32 +++++++++++-------- .../energy/hui-energy-sources-table-card.ts | 1 - 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 50f7e263de..e5f60ad4e0 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -13,6 +13,7 @@ import { TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { getGraphColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { @@ -20,11 +21,10 @@ import { numberFormatToLocale, } from "../../common/number/format_number"; import { - getStatisticIds, getStatisticLabel, + getStatisticMetadata, Statistics, statisticsHaveType, - StatisticsMetaData, StatisticType, } from "../../data/recorder"; import type { HomeAssistant } from "../../types"; @@ -36,8 +36,6 @@ class StatisticsChart extends LitElement { @property({ attribute: false }) public statisticsData!: Statistics; - @property({ type: Array }) public statisticIds?: StatisticsMetaData[]; - @property() public names: boolean | Record = false; @property() public unit?: string; @@ -191,18 +189,28 @@ class StatisticsChart extends LitElement { }; } - private async _getStatisticIds() { - this.statisticIds = await getStatisticIds(this.hass); - } + private _getStatisticsMetaData = memoizeOne( + async (statisticIds: string[] | undefined) => { + const statsMetadataArray = await getStatisticMetadata( + this.hass, + statisticIds + ); + const statisticsMetaData = {}; + statsMetadataArray.forEach((x) => { + statisticsMetaData[x.statistic_id] = x; + }); + return statisticsMetaData; + } + ); private async _generateData() { if (!this.statisticsData) { return; } - if (!this.statisticIds) { - await this._getStatisticIds(); - } + const statisticsMetaData = await this._getStatisticsMetaData( + Object.keys(this.statisticsData) + ); let colorIndex = 0; const statisticsData = Object.values(this.statisticsData); @@ -233,9 +241,7 @@ class StatisticsChart extends LitElement { const names = this.names || {}; statisticsData.forEach((stats) => { const firstStat = stats[0]; - const meta = this.statisticIds!.find( - (stat) => stat.statistic_id === firstStat.statistic_id - ); + const meta = statisticsMetaData?.[firstStat.statistic_id]; let name = names[firstStat.statistic_id]; if (!name) { name = getStatisticLabel(this.hass, firstStat.statistic_id, meta); diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index 64779926c3..dc5eb4b977 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -19,7 +19,6 @@ import { } from "../../../../common/color/convert-color"; import { labBrighten, labDarken } from "../../../../common/color/lab"; import { formatNumber } from "../../../../common/number/format_number"; -import "../../../../components/chart/statistics-chart"; import "../../../../components/ha-card"; import { EnergyData, From e13c632afa3d79393ed1f5f47b69a4140ee09bf8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 15 Sep 2022 16:21:25 +0200 Subject: [PATCH 31/98] Update delete user dialog (#13761) * Add destructive confirmation style * Update delete user dialog --- src/dialogs/generic/dialog-box.ts | 7 +++++++ src/dialogs/generic/show-dialog-box.ts | 1 + src/panels/config/users/ha-config-users.ts | 6 +++++- src/translations/en.json | 5 +++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 27da086a0b..33e5edcbad 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button"; import { mdiAlertOutline } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog"; @@ -96,6 +97,9 @@ class DialogBox extends LitElement { @click=${this._confirm} ?dialogInitialFocus=${!this._params.prompt} slot="primaryAction" + class=${classMap({ + destructive: this._params.destructive || false, + })} > ${this._params.confirmText ? this._params.confirmText @@ -153,6 +157,9 @@ class DialogBox extends LitElement { .secondary { color: var(--secondary-text-color); } + .destructive { + --mdc-theme-primary: var(--error-color); + } ha-dialog { --mdc-dialog-heading-ink-color: var(--primary-text-color); --mdc-dialog-content-ink-color: var(--primary-text-color); diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts index 3180a381f4..6abc0e1dda 100644 --- a/src/dialogs/generic/show-dialog-box.ts +++ b/src/dialogs/generic/show-dialog-box.ts @@ -16,6 +16,7 @@ export interface ConfirmationDialogParams extends BaseDialogBoxParams { dismissText?: string; confirm?: () => void; cancel?: () => void; + destructive?: boolean; } export interface PromptDialogParams extends BaseDialogBoxParams { diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 9001fa82f1..b192716e43 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -207,12 +207,16 @@ export class HaConfigUsers extends LitElement { if ( !(await showConfirmationDialog(this, { title: this.hass!.localize( - "ui.panel.config.users.editor.confirm_user_deletion", + "ui.panel.config.users.editor.confirm_user_deletion_title", "name", entry.name ), + text: this.hass!.localize( + "ui.panel.config.users.editor.confirm_user_deletion_text" + ), dismissText: this.hass!.localize("ui.common.cancel"), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/translations/en.json b/src/translations/en.json index c3fa44cf73..b3fd677e8a 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1293,7 +1293,7 @@ }, "delete": { "confirmation_title": "Are you sure you want to delete this area?", - "confirmation_text": "All devices in this area will become unassigned." + "confirmation_text": "This user will be permanently deleted." } }, "backup": { @@ -2974,7 +2974,8 @@ "system_generated_users_not_removable": "Unable to remove system users.", "system_generated_users_not_editable": "Unable to update system users.", "unnamed_user": "Unnamed User", - "confirm_user_deletion": "Are you sure you want to delete {name}?", + "confirm_user_deletion_title": "Delete {name}?", + "confirm_user_deletion_text": "This user will be permanently deleted.", "active_tooltip": "Controls if user can login" }, "add_user": { From 9e416e829c932c8a77b1f55282eee8c9bafe1672 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 13:38:31 +0200 Subject: [PATCH 32/98] Do not recreate entities list at each re-order (#13751) --- .../lovelace/components/hui-entity-editor.ts | 132 +++++------- .../editor/hui-entities-card-row-editor.ts | 196 ++++++++---------- 2 files changed, 142 insertions(+), 186 deletions(-) diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index e5219ca990..dbfa9b1eaf 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -1,14 +1,7 @@ import { mdiDrag } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { guard } from "lit/directives/guard"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; @@ -30,20 +23,20 @@ export class HuiEntityEditor extends LitElement { @property() protected label?: string; - @state() private _attached = false; - - @state() private _renderEmptySortable = false; + private _entityKeys = new WeakMap(); private _sortable?: SortableInstance; - public connectedCallback() { - super.connectedCallback(); - this._attached = true; + public disconnectedCallback() { + this._destroySortable(); } - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; + private _getKey(action: EntityConfig) { + if (!this._entityKeys.has(action)) { + this._entityKeys.set(action, Math.random().toString()); + } + + return this._entityKeys.get(action)!; } protected render(): TemplateResult { @@ -60,23 +53,23 @@ export class HuiEntityEditor extends LitElement { ")"}
    - ${guard([this.entities, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this.entities!.map( - (entityConf, index) => html` -
    - - -
    - ` - ) + ${repeat( + this.entities, + (entityConf) => this._getKey(entityConf), + (entityConf, index) => html` +
    +
    + +
    + +
    + ` )}
    this._entityMoved(evt), + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._entityMoved(evt); + }, } ); } + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private async _addEntity(ev: CustomEvent): Promise { const value = ev.detail.value; if (value === "") { @@ -198,9 +174,15 @@ export class HuiEntityEditor extends LitElement { display: flex; align-items: center; } - .entity ha-svg-icon { + .entity .handle { padding-right: 8px; cursor: move; + padding-inline-end: 8px; + padding-inline-start: initial; + direction: var(--direction); + } + .entity .handle > * { + pointer-events: none; } .entity ha-entity-picker { flex-grow: 1; diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 3d3925054c..eaf7b4c78b 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -1,14 +1,7 @@ import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { guard } from "lit/directives/guard"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; @@ -39,20 +32,20 @@ export class HuiEntitiesCardRowEditor extends LitElement { @property() protected label?: string; - @state() private _attached = false; - - @state() private _renderEmptySortable = false; + private _entityKeys = new WeakMap(); private _sortable?: SortableInstance; - public connectedCallback() { - super.connectedCallback(); - this._attached = true; + public disconnectedCallback() { + this._destroySortable(); } - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; + private _getKey(action: LovelaceRowConfig) { + if (!this._entityKeys.has(action)) { + this._entityKeys.set(action, Math.random().toString()); + } + + return this._entityKeys.get(action)!; } protected render(): TemplateResult { @@ -70,63 +63,61 @@ export class HuiEntitiesCardRowEditor extends LitElement { )})`}
    - ${guard([this.entities, this._renderEmptySortable], () => - this._renderEmptySortable - ? "" - : this.entities!.map( - (entityConf, index) => html` -
    -
    - + ${repeat( + this.entities, + (entityConf) => this._getKey(entityConf), + (entityConf, index) => html` +
    +
    + +
    + ${entityConf.type + ? html` +
    +
    + + ${this.hass!.localize( + `ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}` + )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.entities.edit_special_row" + )} +
    - ${entityConf.type - ? html` -
    -
    - - ${this.hass!.localize( - `ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}` - )} - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.entities.edit_special_row" - )} -
    -
    - ` - : html` - - `} - - -
    - ` - ) + @value-changed=${this._valueChanged} + > + `} + + +
    + ` )}
    this._rowMoved(evt), + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._rowMoved(evt); + }, } ); } + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private async _addEntity(ev: CustomEvent): Promise { const value = ev.detail.value; if (value === "") { From c93179c307ebe653ac996b5b5fcd1d95e348587d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 16 Sep 2022 13:38:45 +0200 Subject: [PATCH 33/98] Use primary color for active tabs in traces (#13760) --- src/components/trace/trace-tab-styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/trace/trace-tab-styles.ts b/src/components/trace/trace-tab-styles.ts index 40878d304f..4f7146df7a 100644 --- a/src/components/trace/trace-tab-styles.ts +++ b/src/components/trace/trace-tab-styles.ts @@ -28,7 +28,7 @@ export const traceTabStyles = css` } .tabs > *.active { - border-bottom-color: var(--accent-color); + border-bottom-color: var(--primary-color); } .tabs > *:focus, From 589efa8cc5be3497a5831c39745b86a911de7020 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 Sep 2022 13:39:38 +0200 Subject: [PATCH 34/98] Exclude scenes, schedules & updates from scenes (#13759) --- src/data/scene.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/data/scene.ts b/src/data/scene.ts index f15e7c2562..e8f27d6c26 100644 --- a/src/data/scene.ts +++ b/src/data/scene.ts @@ -14,8 +14,11 @@ export const SCENE_IGNORED_DOMAINS = [ "input_button", "persistent_notification", "person", + "scene", + "schedule", "sensor", "sun", + "update", "weather", "zone", ]; From 087a897cbe2e367904052b3860717ab858ad90ac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Sep 2022 13:40:08 +0200 Subject: [PATCH 35/98] Add comment about input number not supporting initial (#13758) --- src/panels/config/helpers/forms/ha-input_number-form.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index a1196cbf22..51e63c28ed 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -33,6 +33,8 @@ class HaInputNumberForm extends LitElement { // eslint-disable-next-line: variable-name @state() private _unit_of_measurement?: string; + /* Configuring initial value is intentionally not supported because the behavior + compared to restoring the value after restart is hard to explain */ set item(item: InputNumber) { this._item = item; if (item) { From bc62e9372b22440feb9a2eb27d337dd624aeb884 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 13:40:41 +0200 Subject: [PATCH 36/98] Update delete dashboard dialog (#13762) --- .../config/lovelace/dashboards/ha-config-lovelace-dashboards.ts | 1 + src/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index 4646d5361b..55af2f7899 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -365,6 +365,7 @@ export class HaConfigLovelaceDashboards extends LitElement { "ui.panel.config.lovelace.dashboards.confirm_delete_text" ), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/translations/en.json b/src/translations/en.json index b3fd677e8a..f700139aa1 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1720,7 +1720,7 @@ "add_dashboard": "Add dashboard" }, "confirm_delete_title": "Delete {dashboard_title}?", - "confirm_delete_text": "Your dashboard will be permanently deleted.", + "confirm_delete_text": "This dashboard will be permanently deleted.", "cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.", "cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.", "detail": { From bb0529ecd2983b4f9473b4f6bf8bc5961707826f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 16 Sep 2022 13:48:10 +0200 Subject: [PATCH 37/98] Improve statistics graph editor (#13630) --- src/components/entity/ha-statistic-picker.ts | 25 +++--- src/components/entity/ha-statistics-picker.ts | 49 +++++++++- src/data/recorder.ts | 16 ++++ .../hui-statistics-graph-card-editor.ts | 90 ++++++++++++++++--- 4 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 1cfabba3d5..ebf129efaa 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -3,6 +3,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { ensureArray } from "../../common/ensure-array"; import { fireEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; import { stringCompare } from "../../common/string/compare"; @@ -39,22 +40,20 @@ export class HaStatisticPicker extends LitElement { type: Array, attribute: "include-statistics-unit-of-measurement", }) - public includeStatisticsUnitOfMeasurement?: string[]; + public includeStatisticsUnitOfMeasurement?: string | string[]; /** * Show only statistics displayed with these units of measurements. - * @type {Array} * @attr include-display-unit-of-measurement */ - @property({ type: Array, attribute: "include-display-unit-of-measurement" }) - public includeDisplayUnitOfMeasurement?: string[]; + @property({ attribute: "include-display-unit-of-measurement" }) + public includeDisplayUnitOfMeasurement?: string | string[]; /** * Show only statistics with these device classes. - * @type {Array} * @attr include-device-classes */ - @property({ type: Array, attribute: "include-device-classes" }) + @property({ attribute: "include-device-classes" }) public includeDeviceClasses?: string[]; /** @@ -97,8 +96,8 @@ export class HaStatisticPicker extends LitElement { private _getStatistics = memoizeOne( ( statisticIds: StatisticsMetaData[], - includeStatisticsUnitOfMeasurement?: string[], - includeDisplayUnitOfMeasurement?: string[], + includeStatisticsUnitOfMeasurement?: string | string[], + includeDisplayUnitOfMeasurement?: string | string[], includeDeviceClasses?: string[], entitiesOnly?: boolean ): Array<{ id: string; name: string; state?: HassEntity }> => { @@ -114,17 +113,15 @@ export class HaStatisticPicker extends LitElement { } if (includeStatisticsUnitOfMeasurement) { + const includeUnits = ensureArray(includeStatisticsUnitOfMeasurement); statisticIds = statisticIds.filter((meta) => - includeStatisticsUnitOfMeasurement.includes( - meta.statistics_unit_of_measurement - ) + includeUnits.includes(meta.statistics_unit_of_measurement) ); } if (includeDisplayUnitOfMeasurement) { + const includeUnits = ensureArray(includeDisplayUnitOfMeasurement); statisticIds = statisticIds.filter((meta) => - includeDisplayUnitOfMeasurement.includes( - meta.display_unit_of_measurement - ) + includeUnits.includes(meta.display_unit_of_measurement) ); } diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index aa25e45803..ef450d4ae2 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -22,11 +22,52 @@ class HaStatisticsPicker extends LitElement { @property({ attribute: "pick-statistic-label" }) public pickStatisticLabel?: string; + /** + * Show only statistics natively stored with these units of measurements. + * @attr include-statistics-unit-of-measurement + */ + @property({ + attribute: "include-statistics-unit-of-measurement", + }) + public includeStatisticsUnitOfMeasurement?: string[] | string; + + /** + * Show only statistics displayed with these units of measurements. + * @attr include-display-unit-of-measurement + */ + @property({ attribute: "include-display-unit-of-measurement" }) + public includeDisplayUnitOfMeasurement?: string[] | string; + + /** + * Ignore filtering of statistics type and units when only a single statistic is selected. + * @type {boolean} + * @attr ignore-restrictions-on-first-statistic + */ + @property({ + type: Boolean, + attribute: "ignore-restrictions-on-first-statistic", + }) + public ignoreRestrictionsOnFirstStatistic = false; + protected render(): TemplateResult { if (!this.hass) { return html``; } + const ignoreRestriction = + this.ignoreRestrictionsOnFirstStatistic && + this._currentStatistics.length <= 1; + + const includeDisplayUnitCurrent = ignoreRestriction + ? undefined + : this.includeDisplayUnitOfMeasurement; + const includeStatisticsUnitCurrent = ignoreRestriction + ? undefined + : this.includeStatisticsUnitOfMeasurement; + const includeStatisticTypesCurrent = ignoreRestriction + ? undefined + : this.statisticTypes; + return html` ${this._currentStatistics.map( (statisticId) => html` @@ -34,8 +75,10 @@ class HaStatisticsPicker extends LitElement { stats.some((stat) => stat[type] !== null); +const mean_stat_types: readonly StatisticType[] = ["mean", "min", "max"]; +const sum_stat_types: readonly StatisticType[] = ["sum"]; + +export const statisticsMetaHasType = ( + metadata: StatisticsMetaData, + type: StatisticType +) => { + if (mean_stat_types.includes(type) && metadata.has_mean) { + return true; + } + if (sum_stat_types.includes(type) && metadata.has_sum) { + return true; + } + return false; +}; + export const adjustStatisticsSum = ( hass: HomeAssistant, statistic_id: string, diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index 879a6c8a41..0d4dd1b21d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -1,5 +1,12 @@ import "../../../../components/ha-form/ha-form"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { @@ -23,6 +30,12 @@ import { processConfigEntities } from "../../common/process-config-entities"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entitiesConfigStruct } from "../structs/entities-struct"; +import { + getStatisticMetadata, + StatisticsMetaData, + statisticsMetaHasType, +} from "../../../../data/recorder"; +import { deepEqual } from "../../../../common/util/deep-equal"; const statTypeStruct = union([ literal("sum"), @@ -51,6 +64,13 @@ const cardConfigStruct = assign( ); const periods = ["5minute", "hour", "day", "month"] as const; +const stat_types = ["mean", "min", "max", "sum"] as const; +const stat_type_labels = { + mean: "Mean", + min: "Min", + max: "Max", + sum: "Sum", +} as const; @customElement("hui-statistics-graph-card-editor") export class HuiStatisticsGraphCardEditor @@ -63,6 +83,8 @@ export class HuiStatisticsGraphCardEditor @state() private _configEntities?: string[]; + @state() private _metaDatas?: StatisticsMetaData[]; + public setConfig(config: StatisticsGraphCardConfig): void { assert(config, cardConfigStruct); this._config = config; @@ -71,8 +93,29 @@ export class HuiStatisticsGraphCardEditor : []; } + private _getStatisticsMetaData = async (statisticIds?: string[]) => { + this._metaDatas = await getStatisticMetadata( + this.hass!, + statisticIds || [] + ); + }; + + public willUpdate(changedProps: PropertyValues) { + if ( + changedProps.has("_configEntities") && + !deepEqual(this._configEntities, changedProps.get("_configEntities")) + ) { + this._metaDatas = undefined; + this._getStatisticsMetaData(this._configEntities); + } + } + private _schema = memoizeOne( - (localize: LocalizeFunc) => + ( + localize: LocalizeFunc, + statisticIds: string[] | undefined, + metaDatas: StatisticsMetaData[] | undefined + ) => [ { name: "title", selector: { text: {} } }, { @@ -89,6 +132,13 @@ export class HuiStatisticsGraphCardEditor label: localize( `ui.panel.lovelace.editor.card.statistics-graph.periods.${period}` ), + disabled: + period === "5minute" && + // External statistics don't support 5-minute statistics. + // External statistics is formatted as : + statisticIds?.some((statistic_id) => + statistic_id.includes(":") + ), })), }, }, @@ -101,13 +151,20 @@ export class HuiStatisticsGraphCardEditor { name: "stat_types", required: true, - type: "multi_select", - options: [ - ["mean", "Mean"], - ["min", "Min"], - ["max", "Max"], - ["sum", "Sum"], - ], + selector: { + select: { + multiple: true, + options: stat_types.map((stat_type) => ({ + value: stat_type, + label: stat_type_labels[stat_type], + disabled: + !metaDatas || + !metaDatas?.every((metaData) => + statisticsMetaHasType(metaData, stat_type) + ), + })), + }, + }, }, { name: "chart_type", @@ -128,19 +185,24 @@ export class HuiStatisticsGraphCardEditor return html``; } - const schema = this._schema(this.hass.localize); - const stat_types = this._config!.stat_types + const schema = this._schema( + this.hass.localize, + this._configEntities, + this._metaDatas + ); + const configured_stat_types = this._config!.stat_types ? Array.isArray(this._config!.stat_types) ? this._config!.stat_types : [this._config!.stat_types] - : ["mean", "min", "max", "sum"]; + : stat_types; const data = { chart_type: "line", period: "hour", days_to_show: 30, ...this._config, - stat_types, + stat_types: configured_stat_types, }; + const displayUnit = this._metaDatas?.[0]?.display_unit_of_measurement; return html` Date: Fri, 16 Sep 2022 13:48:55 +0200 Subject: [PATCH 38/98] Update reboot shutdown host dialog (#13777) --- .../config/hardware/ha-config-hardware.ts | 16 +++++++-------- src/translations/en.json | 20 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 2d225bca1c..f04b06e039 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -376,10 +376,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { private async _hostReboot(): Promise { const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.reboot_host"), - text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"), - confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"), + title: this.hass.localize("ui.panel.config.hardware.reboot_host_title"), + text: this.hass.localize("ui.panel.config.hardware.reboot_host_text"), + confirmText: this.hass.localize("ui.panel.config.hardware.reboot"), dismissText: this.hass.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { @@ -408,12 +409,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { private async _hostShutdown(): Promise { const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.panel.config.hardware.shutdown_host"), - text: this.hass.localize( - "ui.panel.config.hardware.shutdown_host_confirm" - ), - confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"), + title: this.hass.localize("ui.panel.config.hardware.shutdown_host_title"), + text: this.hass.localize("ui.panel.config.hardware.shutdown_host_text"), + confirmText: this.hass.localize("ui.panel.config.hardware.shutdown"), dismissText: this.hass.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { diff --git a/src/translations/en.json b/src/translations/en.json index f700139aa1..64b3974457 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1622,14 +1622,18 @@ }, "processor": "Processor", "memory": "Memory", - "reboot_host": "Reboot host", - "rebooting_host": "Rebooting host", - "reboot_host_confirm": "Are you sure you want to reboot your host?", - "failed_to_reboot_host": "Failed to reboot host", - "shutdown_host": "Shutdown host", - "host_shutting_down": "Host shutting down", - "shutdown_host_confirm": "Are you sure you want to shutdown your host?", - "failed_to_shutdown_host": "Failed to shutdown host", + "rebooting_host": "Rebooting system", + "reboot": "Reboot", + "reboot_host": "Reboot system", + "reboot_host_title": "Reboot system?", + "reboot_host_text": "This will reboot the complete system which includes the Core and all Add-ons.", + "failed_to_reboot_host": "Failed to reboot system", + "host_shutting_down": "system shutting down", + "shutdown": "Shutdown", + "shutdown_host": "Shutdown system", + "shutdown_host_title": "Shutdown system?", + "shutdown_host_text": "This will shutdown the complete system which includes the Core and all Add-ons.", + "failed_to_shutdown_host": "Failed to shutdown system", "board": "Board", "documentation": "Documentation", "documentation_description": "Find extra information about your device" From 5a5f31b32c7ac29accc24c1afe7e84cb63bfa036 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 13:49:15 +0200 Subject: [PATCH 39/98] Update restart Home Assistant dialog (#13776) --- src/panels/config/core/ha-config-system-navigation.ts | 1 + src/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index 2ac869d2d5..22c5250849 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -181,6 +181,7 @@ class HaConfigSystemNavigation extends LitElement { }); }); }, + destructive: true, }); } diff --git a/src/translations/en.json b/src/translations/en.json index 64b3974457..a15efafbe8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3516,7 +3516,7 @@ "integration_start_time": "Integration Startup Time" }, "system_dashboard": { - "confirm_restart_text": "Restarting Home Assistant will stop all your active dashboards, automations and scripts.", + "confirm_restart_text": "This will stop all your active dashboards, automations and scripts.", "confirm_restart_title": "Restart Home Assistant?", "restart_homeassistant_short": "Restart", "restart_error": "Failed to restart Home Assistant" From 0ba4a07b92c6cd8fc8f986dfe53bbecbf8abae0e Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 13:50:44 +0200 Subject: [PATCH 40/98] Update webhook dialog (#13774) --- .../dialog-manage-cloudhook.ts | 111 ++++++++++++------ src/translations/en.json | 6 +- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index 1dc48f780f..d9dce1d378 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -1,18 +1,19 @@ import "@material/mwc-button"; +import { mdiContentCopy, mdiOpenInNew } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { query, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { copyToClipboard } from "../../../../common/util/copy-clipboard"; -import type { HaTextField } from "../../../../components/ha-textfield"; +import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../components/ha-textfield"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; import { documentationUrl } from "../../../../util/documentation-url"; +import { showToast } from "../../../../util/toast"; import { WebhookDialogParams } from "./show-dialog-manage-cloudhook"; -const inputLabel = "Public URL – Click to copy to clipboard"; - export class DialogManageCloudhook extends LitElement { protected hass?: HomeAssistant; @@ -44,26 +45,19 @@ export class DialogManageCloudhook extends LitElement { return html`

    - ${this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.available_at" - )} -

    - -

    - ${cloudhook.managed + ${!cloudhook.managed ? html` ${this.hass!.localize( "ui.panel.config.cloud.dialog_cloudhook.managed_by_integration" @@ -79,7 +73,29 @@ export class DialogManageCloudhook extends LitElement { )}. `} +
    + + ${this.hass!.localize( + "ui.panel.config.cloud.dialog_cloudhook.view_documentation" + )} + +

    + + +
    { - this._params!.disableHook(); - this.closeDialog(); - }, + destructive: true, }); - } - - private _copyClipboard(ev: FocusEvent) { - const textField = ev.currentTarget as HaTextField; - try { - copyToClipboard(textField.value); - textField.label = this.hass!.localize( - "ui.panel.config.cloud.dialog_cloudhook.copied_to_clipboard" - ); - } catch (err: any) { - // Copying failed. Oh no + if (confirmed) { + this._params!.disableHook(); + this.closeDialog(); } } - private _restoreLabel() { - this._input.label = inputLabel; + private focusInput(ev) { + const inputElement = ev.currentTarget as HaTextField; + inputElement.select(); + } + + private async _copyUrl(ev): Promise { + if (!this.hass) return; + ev.stopPropagation(); + const inputElement = ev.target.parentElement as HaTextField; + inputElement.select(); + const url = this.hass.hassUrl(inputElement.value); + + await copyToClipboard(url); + showToast(this, { + message: this.hass.localize("ui.common.copied_clipboard"), + }); } static get styles(): CSSResultGroup { @@ -142,12 +165,24 @@ export class DialogManageCloudhook extends LitElement { ha-textfield { display: block; } + ha-textfield > ha-icon-button { + --mdc-icon-button-size: 24px; + --mdc-icon-size: 18px; + } button.link { color: var(--primary-color); + text-decoration: none; } a { text-decoration: none; } + a ha-svg-icon { + --mdc-icon-size: 16px; + } + p { + margin-top: 0; + margin-bottom: 16px; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index a15efafbe8..2568636938 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2558,14 +2558,14 @@ }, "dialog_cloudhook": { "webhook_for": "Webhook for {name}", - "available_at": "The webhook is available at the following URL:", "managed_by_integration": "This webhook is managed by an integration and cannot be disabled.", "info_disable_webhook": "If you no longer want to use this webhook, you can", "link_disable_webhook": "disable it", + "public_url": "Public address", "view_documentation": "View documentation", "close": "Close", - "confirm_disable": "Are you sure you want to disable this webhook?", - "copied_to_clipboard": "Copied to clipboard" + "confirm_disable_title": "Disable webhook", + "confirm_disable_text": "Webhook for {name} will be disabled." } }, "devices": { From e1e3f9d9257a11a6ede36f965f3c9321d99833a9 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 13:52:26 +0200 Subject: [PATCH 41/98] Update delete and disable integration dialog (#13772) --- .../integrations/ha-integration-card.ts | 21 +++++++++++++++---- src/translations/en.json | 8 ++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 59d61f189a..907ea34a68 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -634,9 +634,16 @@ export class HaIntegrationCard extends LitElement { const entryId = configEntry.entry_id; const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.disable.disable_confirm" + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.disable_confirm_title", + { title: configEntry.title } ), + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.disable_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.disable"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { @@ -692,10 +699,16 @@ export class HaIntegrationCard extends LitElement { const entryId = configEntry.entry_id; const confirmed = await showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_entry.delete_confirm", + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm_title", { title: configEntry.title } ), + text: this.hass.localize( + "ui.panel.config.integrations.config_entry.delete_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.delete"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, }); if (!confirmed) { diff --git a/src/translations/en.json b/src/translations/en.json index 2568636938..4afa6ffc7d 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2857,13 +2857,16 @@ "download_diagnostics": "Download diagnostics", "known_issues": "Known issues", "delete": "Delete", - "delete_confirm": "Are you sure you want to delete the {title} integration?", + "delete_confirm_title": "Delete {title}?", + "delete_confirm_text": "Its devices and entities will be permantly deleted.", "reload": "Reload", "restart_confirm": "Restart Home Assistant to finish removing this integration", "reload_confirm": "The integration was reloaded", "reload_restart_confirm": "Restart Home Assistant to finish reloading this integration", "disable_restart_confirm": "Restart Home Assistant to finish disabling this integration", "enable_restart_confirm": "Restart Home Assistant to finish enabling this integration", + "disable_confirm_title": "Disable {title}?", + "disable_confirm_text": "Its devices and entities will be disabled.", "disable_error": "Enabling or disabling of the integration failed", "manuf": "by {manufacturer}", "via": "Connected via", @@ -2884,8 +2887,7 @@ "user": "user", "integration": "integration", "device": "device" - }, - "disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled." + } }, "provided_by_custom_integration": "Provided by a custom integration", "depends_on_cloud": "Depends on the cloud", From 614c1574ca74769e36cb8028685fee1f6cd7c368 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Sep 2022 08:33:21 -0400 Subject: [PATCH 42/98] Type outgoing messages on EM and add matter (#13775) --- src/components/ha-hls-player.ts | 2 +- src/external_app/external_app_entrypoint.ts | 4 +- src/external_app/external_messaging.ts | 129 ++++++++++++++++---- 3 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts index 0be5f97535..70a91b7e5e 100644 --- a/src/components/ha-hls-player.ts +++ b/src/components/ha-hls-player.ts @@ -165,7 +165,7 @@ class HaHLSPlayer extends LitElement { window.addEventListener("resize", this._resizeExoPlayer); this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); this._videoEl.style.visibility = "hidden"; - await this.hass!.auth.external!.sendMessage({ + await this.hass!.auth.external!.fireMessage({ type: "exoplayer/play_hls", payload: { url: new URL(url, window.location.href).toString(), diff --git a/src/external_app/external_app_entrypoint.ts b/src/external_app/external_app_entrypoint.ts index 46d5a39dee..0f059a11ae 100644 --- a/src/external_app/external_app_entrypoint.ts +++ b/src/external_app/external_app_entrypoint.ts @@ -7,7 +7,7 @@ This is the entry point for providing external app stuff from app entrypoint. import { fireEvent } from "../common/dom/fire_event"; import { HomeAssistantMain } from "../layouts/home-assistant-main"; -import type { EMExternalMessageCommands } from "./external_messaging"; +import type { EMIncomingMessageCommands } from "./external_messaging"; export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => { window.addEventListener("haptic", (ev) => @@ -24,7 +24,7 @@ export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => { const handleExternalMessage = ( hassMainEl: HomeAssistantMain, - msg: EMExternalMessageCommands + msg: EMIncomingMessageCommands ): boolean => { const bus = hassMainEl.hass.auth.external!; diff --git a/src/external_app/external_messaging.ts b/src/external_app/external_messaging.ts index fdab1ab2eb..2bf8fa34fb 100644 --- a/src/external_app/external_messaging.ts +++ b/src/external_app/external_messaging.ts @@ -8,7 +8,6 @@ interface CommandInFlight { export interface EMMessage { id?: number; type: string; - payload?: unknown; } interface EMError { @@ -30,34 +29,120 @@ interface EMMessageResultError { error: EMError; } -interface EMExternalMessageRestart { +interface EMOutgoingMessageConfigGet extends EMMessage { + type: "config/get"; +} + +interface EMOutgoingMessageMatterCommission extends EMMessage { + type: "matter/commission"; +} + +type EMOutgoingMessageWithAnswer = { + "config/get": { + request: EMOutgoingMessageConfigGet; + response: ExternalConfig; + }; + "matter/commission": { + request: EMOutgoingMessageMatterCommission; + response: { + code: string; + }; + }; +}; + +interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage { + type: "exoplayer/play_hls"; + payload: { + url: string; + muted: boolean; + }; +} +interface EMOutgoingMessageExoplayerResize extends EMMessage { + type: "exoplayer/resize"; + payload: { + left: number; + top: number; + right: number; + bottom: number; + }; +} + +interface EMOutgoingMessageExoplayerStop extends EMMessage { + type: "exoplayer/stop"; +} + +interface EMOutgoingMessageThemeUpdate extends EMMessage { + type: "theme-update"; +} + +interface EMOutgoingMessageHaptic extends EMMessage { + type: "haptic"; + payload: { hapticType: string }; +} + +interface EMOutgoingMessageConnectionStatus extends EMMessage { + type: "connection-status"; + payload: { event: string }; +} + +interface EMOutgoingMessageAppConfiguration extends EMMessage { + type: "config_screen/show"; +} + +interface EMOutgoingMessageTagWrite extends EMMessage { + type: "tag/write"; + payload: { + name: string | null; + tag: string; + }; +} + +interface EMOutgoingMessageSidebarShow extends EMMessage { + type: "sidebar/show"; +} + +type EMOutgoingMessageWithoutAnswer = + | EMOutgoingMessageHaptic + | EMOutgoingMessageConnectionStatus + | EMOutgoingMessageAppConfiguration + | EMOutgoingMessageTagWrite + | EMOutgoingMessageSidebarShow + | EMOutgoingMessageExoplayerPlayHLS + | EMOutgoingMessageExoplayerResize + | EMOutgoingMessageExoplayerStop + | EMOutgoingMessageThemeUpdate + | EMMessageResultSuccess + | EMMessageResultError; + +interface EMIncomingMessageRestart { id: number; type: "command"; command: "restart"; } -interface EMExternMessageShowNotifications { +interface EMIncomingMessageShowNotifications { id: number; type: "command"; command: "notifications/show"; } -export type EMExternalMessageCommands = - | EMExternalMessageRestart - | EMExternMessageShowNotifications; +export type EMIncomingMessageCommands = + | EMIncomingMessageRestart + | EMIncomingMessageShowNotifications; -type ExternalMessage = +type EMIncomingMessage = | EMMessageResultSuccess | EMMessageResultError - | EMExternalMessageCommands; + | EMIncomingMessageCommands; -type ExternalMessageHandler = (msg: EMExternalMessageCommands) => boolean; +type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean; export interface ExternalConfig { hasSettingsScreen: boolean; hasSidebar: boolean; canWriteTag: boolean; hasExoPlayer: boolean; + canCommissionMatter: boolean; } export class ExternalMessaging { @@ -67,7 +152,7 @@ export class ExternalMessaging { public msgId = 0; - private _commandHandler?: ExternalMessageHandler; + private _commandHandler?: EMIncomingMessageHandler; public async attach() { window[CALLBACK_EXTERNAL_BUS] = (msg) => this.receiveMessage(msg); @@ -77,12 +162,12 @@ export class ExternalMessaging { payload: { event: ev.detail }, }) ); - this.config = await this.sendMessage({ + this.config = await this.sendMessage<"config/get">({ type: "config/get", }); } - public addCommandHandler(handler: ExternalMessageHandler) { + public addCommandHandler(handler: EMIncomingMessageHandler) { this._commandHandler = handler; } @@ -90,31 +175,33 @@ export class ExternalMessaging { * Send message to external app that expects a response. * @param msg message to send */ - public sendMessage(msg: EMMessage): Promise { + public sendMessage( + msg: EMOutgoingMessageWithAnswer[T]["request"] + ): Promise { const msgId = ++this.msgId; msg.id = msgId; - this.fireMessage(msg); + this._sendExternal(msg); - return new Promise((resolve, reject) => { - this.commands[msgId] = { resolve, reject }; - }); + return new Promise( + (resolve, reject) => { + this.commands[msgId] = { resolve, reject }; + } + ); } /** * Send message to external app without expecting a response. * @param msg message to send */ - public fireMessage( - msg: EMMessage | EMMessageResultSuccess | EMMessageResultError - ) { + public fireMessage(msg: EMOutgoingMessageWithoutAnswer) { if (!msg.id) { msg.id = ++this.msgId; } this._sendExternal(msg); } - public receiveMessage(msg: ExternalMessage) { + public receiveMessage(msg: EMIncomingMessage) { if (__DEV__) { // eslint-disable-next-line no-console console.log("Receiving message from external app", msg); From 3b103619ec23f4162398fd74bd52196b3d6a5727 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:38:29 +0200 Subject: [PATCH 43/98] Update delete resource dialog (#13764) --- .../resources/ha-config-lovelace-resources.ts | 11 +++++++++-- src/translations/en.json | 3 ++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 5aaad37f09..b93afcc17d 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -157,9 +157,16 @@ export class HaConfigLovelaceRescources extends LitElement { removeResource: async () => { if ( !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.lovelace.resources.confirm_delete" + title: this.hass!.localize( + "ui.panel.config.lovelace.resources.confirm_delete_title" ), + text: this.hass!.localize( + "ui.panel.config.lovelace.resources.confirm_delete_text", + { url: resource!.url } + ), + dismissText: this.hass!.localize("ui.common.cancel"), + confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/translations/en.json b/src/translations/en.json index 4afa6ffc7d..78e6061de7 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1761,7 +1761,8 @@ "no_resources": "No resources", "add_resource": "Add resource" }, - "confirm_delete": "Are you sure you want to delete this resource?", + "confirm_delete_title": "Delete resource?", + "confirm_delete_text": "{url} will be permanently deleted.", "refresh_header": "Do you want to refresh?", "refresh_body": "You have to refresh the page to complete the removal. Do you want to refresh now?", "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", From dcfcd54f10998071081899438c22dbcbaae4d8bd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:39:29 +0200 Subject: [PATCH 44/98] Update delete scene dialog (#13767) --- src/panels/config/scene/ha-scene-dashboard.ts | 16 ++++++++++------ src/panels/config/scene/ha-scene-editor.ts | 10 +++++++++- src/translations/en.json | 3 ++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index cd31e69268..14830179ec 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -268,18 +268,22 @@ class HaSceneDashboard extends LitElement { private _activateScene = async (scene: SceneEntity) => { await activateScene(this.hass, scene.entity_id); showToast(this, { - message: this.hass.localize( - "ui.panel.config.scene.activated", - "name", - computeStateName(scene) - ), + message: this.hass.localize("ui.panel.config.scene.activated", { + name: computeStateName(scene), + }), }); forwardHaptic("light"); }; private _deleteConfirm(scene: SceneEntity): void { showConfirmationDialog(this, { - text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"), + title: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_text", + { name: computeStateName(scene) } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(scene), diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 0fb724c635..b1f1e47599 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -785,10 +785,18 @@ export class HaSceneEditor extends SubscribeMixin( private _deleteTapped(): void { showConfirmationDialog(this, { - text: this.hass!.localize("ui.panel.config.scene.picker.delete_confirm"), + title: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.picker.delete_confirm_text", + "name", + this._config?.name + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), + destructive: true, }); } diff --git a/src/translations/en.json b/src/translations/en.json index 78e6061de7..e15a9985d5 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2338,7 +2338,8 @@ "activate": "Activate", "delete_scene": "Delete scene", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this scene?", + "delete_confirm_title": "Delete scene?", + "delete_confirm_text": "{name} will be permanently deleted.", "duplicate_scene": "Duplicate scene", "duplicate": "[%key:ui::common::duplicate%]", "headers": { From 7ffd30643ad27275680a576aa5f818a1178d83a2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:39:51 +0200 Subject: [PATCH 45/98] Update delete person dialog (#13763) --- src/panels/config/person/ha-config-person.ts | 10 ++++++++-- src/translations/en.json | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 4546e79507..87d16caf94 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -233,10 +233,16 @@ class HaConfigPerson extends LitElement { removeEntry: async () => { if ( !(await showConfirmationDialog(this, { - title: this.hass!.localize("ui.panel.config.person.confirm_delete"), - text: this.hass!.localize("ui.panel.config.person.confirm_delete2"), + title: this.hass!.localize( + "ui.panel.config.person.confirm_delete_title", + { name: entry!.name } + ), + text: this.hass!.localize( + "ui.panel.config.person.confirm_delete_text" + ), dismissText: this.hass!.localize("ui.common.cancel"), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, })) ) { return false; diff --git a/src/translations/en.json b/src/translations/en.json index e15a9985d5..8d2387eb14 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2749,8 +2749,8 @@ "no_persons_created_yet": "Looks like you have not added any people yet.", "create_person": "Create Person", "add_person": "Add Person", - "confirm_delete": "Are you sure you want to delete this person?", - "confirm_delete2": "All devices belonging to this person will become unassigned.", + "confirm_delete_title": "Delete {name}?", + "confirm_delete_text": "This person will be permanently deleted and all devices belonging to this person will become unassigned.", "person_not_found_title": "Person Not Found", "person_not_found": "We couldn't find the person you were trying to edit.", "detail": { From c5f4e8ffdd4e1f88dc763dc037557b9a83939250 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:40:10 +0200 Subject: [PATCH 46/98] Update certificate information dialog (#13773) --- .../dialog-cloud-certificate.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts index da05e069d8..a17cd270fc 100644 --- a/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts +++ b/src/panels/config/cloud/dialog-cloud-certificate/dialog-cloud-certificate.ts @@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../components/ha-dialog"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate"; @@ -32,8 +33,13 @@ class DialogCloudCertificate extends LitElement { return html`
    @@ -76,6 +82,13 @@ class DialogCloudCertificate extends LitElement { .break-word { overflow-wrap: break-word; } + p { + margin-top: 0; + margin-bottom: 12px; + } + p:last-child { + margin-bottom: 0; + } `, ]; } From a97dfbb51fa114c3e2576d102854057a1f3bcb6c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:40:25 +0200 Subject: [PATCH 47/98] Update delete blueprint dialog (#13770) --- src/panels/config/blueprint/ha-blueprint-overview.ts | 9 +++++++-- src/translations/en.json | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 3df0c44e41..ff1280e361 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -329,11 +329,16 @@ class HaBlueprintOverview extends LitElement { if ( !(await showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.config.blueprint.overview.confirm_delete_header" + "ui.panel.config.blueprint.overview.confirm_delete_title" ), text: this.hass.localize( - "ui.panel.config.blueprint.overview.confirm_delete_text" + "ui.panel.config.blueprint.overview.confirm_delete_text", + "name", + { name: blueprint.name } ), + confirmText: this.hass!.localize("ui.common.delete"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, })) ) { return; diff --git a/src/translations/en.json b/src/translations/en.json index 8d2387eb14..d0afe018f6 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2239,8 +2239,8 @@ "automation": "Automation", "script": "Script" }, - "confirm_delete_header": "Delete this blueprint?", - "confirm_delete_text": "Are you sure you want to delete this blueprint?", + "confirm_delete_title": "Delete blueprint?", + "confirm_delete_text": "{name} will be permanently deleted.", "add_blueprint": "Import blueprint", "no_blueprints": "[%key:ui::panel::config::automation::editor::blueprint::no_blueprints%]", "create_automation": "Create automation", From 5fcb219fcd8b5d35bd58b1e9b076b2f4cde27e77 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 16 Sep 2022 16:40:41 +0200 Subject: [PATCH 48/98] Update delete script dialog (#13768) --- src/panels/config/script/ha-script-editor.ts | 9 ++++++++- src/panels/config/script/ha-script-picker.ts | 10 +++++++++- src/translations/en.json | 3 ++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index d39e81a473..187b9d1687 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -697,10 +697,17 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private async _deleteConfirm() { showConfirmationDialog(this, { - text: this.hass.localize("ui.panel.config.script.editor.delete_confirm"), + title: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_text", + { name: this._config?.alias } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), + destructive: true, }); } diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index da705e7ef4..b3b78028de 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -322,10 +322,18 @@ class HaScriptPicker extends LitElement { private async _deleteConfirm(script: any) { showConfirmationDialog(this, { - text: this.hass.localize("ui.panel.config.script.editor.delete_confirm"), + title: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.script.editor.delete_confirm_text", + "name", + { name: script.name } + ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(script), + destructive: true, }); } diff --git a/src/translations/en.json b/src/translations/en.json index d0afe018f6..002316d2ad 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2313,7 +2313,8 @@ "load_error_not_duplicable": "Only scripts in scripts.yaml can be duplicated.", "load_error_not_deletable": "Only scripts in scripts.yaml can be deleted.", "load_error_unknown": "Error loading script ({err_no}).", - "delete_confirm": "Are you sure you want to delete this script?", + "delete_confirm_title": "Delete script?", + "delete_confirm_text": "{name} will be permanently deleted.", "save_script": "Save script", "sequence": "Sequence", "sequence_sentence": "The sequence of actions of this script.", From aee11da671453c8622c05568a4e08aab6bc4679a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 09:22:59 +0200 Subject: [PATCH 49/98] Bump actions/stale from 5.1.1 to 5.2.0 (#13805) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 880e043c7f..c55578ffec 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 90 days stale policy - uses: actions/stale@v5.1.1 + uses: actions/stale@v5.2.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 From 063c37779760b9974982fa509f0cd251d8cf424d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 10:10:50 +0200 Subject: [PATCH 50/98] Fix back button on automation editor on Safari (#13806) --- src/panels/config/automation/ha-automation-editor.ts | 3 ++- src/panels/config/script/ha-script-editor.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 14f34c7f6b..0fd4eaaf80 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -31,6 +31,7 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -553,7 +554,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private _backTapped = async () => { const result = await this.confirmUnsavedChanged(); if (result) { - history.back(); + afterNextRender(() => history.back()); } }; diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 187b9d1687..907351e269 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -29,6 +29,7 @@ import { navigate } from "../../../common/navigate"; import { slugify } from "../../../common/string/slugify"; import { computeRTL } from "../../../common/util/compute_rtl"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; @@ -679,7 +680,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _backTapped = async () => { const result = await this.confirmUnsavedChanged(); if (result) { - history.back(); + afterNextRender(() => history.back()); } }; From 05515f21c360d5632e84b539f19f09b58055c76c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Sep 2022 10:11:10 +0200 Subject: [PATCH 51/98] Add timer states to state selector (#13788) --- src/common/entity/get_states.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts index f79cc1a4d0..7c59eb2faa 100644 --- a/src/common/entity/get_states.ts +++ b/src/common/entity/get_states.ts @@ -37,6 +37,7 @@ const FIXED_DOMAIN_STATES = { siren: ["on", "off"], sun: ["above_horizon", "below_horizon"], switch: ["on", "off"], + timer: ["active", "idle", "paused"], update: ["on", "off"], vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"], weather: [ From 50b67751d98f5a21de083d6cbfd5b1f5ab0f07a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 19 Sep 2022 10:11:56 +0200 Subject: [PATCH 52/98] Fix time trigger description pointing to entity (#13786) --- src/data/automation_i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index e81e6c992a..8b17c894ed 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -189,7 +189,7 @@ export const describeTrigger = ( // Time Trigger if (trigger.platform === "time" && trigger.at) { const at = trigger.at.includes(".") - ? hass.states[trigger.at] || trigger.at + ? `entity ${computeStateName(hass.states[trigger.at]) || trigger.at}` : trigger.at; return `When the time is equal to ${at}`; From 0ce695577c699b6829b1f8c4a223b71904bfcb12 Mon Sep 17 00:00:00 2001 From: Salamandar <6552989+Salamandar@users.noreply.github.com> Date: Mon, 19 Sep 2022 10:21:45 +0200 Subject: [PATCH 53/98] Fix shortcuts for non-qwerty keyboard layouts (revert #12892) (#13190) --- src/state/quick-bar-mixin.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index b359c70e7f..8882cbcd3b 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -48,6 +48,11 @@ export default >(superClass: T) => private _registerShortcut() { tinykeys(window, { + // Those are for latin keyboards that have e, c, m keys + e: (ev) => this._showQuickBar(ev), + c: (ev) => this._showQuickBar(ev, true), + m: (ev) => this._createMyLink(ev), + // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) KeyE: (ev) => this._showQuickBar(ev), KeyC: (ev) => this._showQuickBar(ev, true), KeyM: (ev) => this._createMyLink(ev), From e8ce6ad919120b44cfa4827203dc5407129e8e31 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 11:35:33 +0200 Subject: [PATCH 54/98] Improve confirm unsaved dialog (#13807) --- .../config/automation/ha-automation-editor.ts | 6 +- src/panels/config/scene/ha-scene-editor.ts | 70 +++++++++---------- src/panels/config/script/ha-script-editor.ts | 6 +- src/translations/en.json | 6 +- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 0fd4eaaf80..627a052450 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -541,11 +541,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private async confirmUnsavedChanged(): Promise { if (this._dirty) { return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.automation.editor.unsaved_confirm_title" + ), text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm" + "ui.panel.config.automation.editor.unsaved_confirm_text" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, }); } return true; diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index b1f1e47599..40ccc29163 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -26,6 +26,7 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/device/ha-device-picker"; import "../../../components/entity/ha-entities-picker"; import "../../../components/ha-area-picker"; @@ -763,24 +764,16 @@ export class HaSceneEditor extends SubscribeMixin( } } - private _backTapped = (): void => { - if (this._dirty) { - showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.scene.editor.unsaved_confirm" - ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - confirm: () => this._goBack(), - }); - } else { + private _backTapped = async (): Promise => { + const result = await this.confirmUnsavedChanged(); + if (result) { this._goBack(); } }; private _goBack(): void { applyScene(this.hass, this._storedStates); - history.back(); + afterNextRender(() => history.back()); } private _deleteTapped(): void { @@ -806,32 +799,37 @@ export class HaSceneEditor extends SubscribeMixin( history.back(); } - private async _duplicate() { + private async confirmUnsavedChanged(): Promise { if (this._dirty) { - if ( - !(await showConfirmationDialog(this, { - text: this.hass!.localize( - "ui.panel.config.scene.editor.unsaved_confirm" - ), - confirmText: this.hass!.localize("ui.common.leave"), - dismissText: this.hass!.localize("ui.common.stay"), - })) - ) { - return; - } - // Wait for dialog to complete closing - await new Promise((resolve) => setTimeout(resolve, 0)); + return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.scene.editor.unsaved_confirm_title" + ), + text: this.hass!.localize( + "ui.panel.config.scene.editor.unsaved_confirm_text" + ), + confirmText: this.hass!.localize("ui.common.leave"), + dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, + }); + } + return true; + } + + private async _duplicate() { + const result = await this.confirmUnsavedChanged(); + if (result) { + showSceneEditor( + { + ...this._config, + id: undefined, + name: `${this._config?.name} (${this.hass.localize( + "ui.panel.config.scene.picker.duplicate" + )})`, + }, + this._sceneAreaIdCurrent || undefined + ); } - showSceneEditor( - { - ...this._config, - id: undefined, - name: `${this._config?.name} (${this.hass.localize( - "ui.panel.config.scene.picker.duplicate" - )})`, - }, - this._sceneAreaIdCurrent || undefined - ); } private _calculateMetaData(): SceneMetaData { diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 907351e269..7029ba4e3f 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -667,11 +667,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private async confirmUnsavedChanged(): Promise { if (this._dirty) { return showConfirmationDialog(this, { + title: this.hass!.localize( + "ui.panel.config.automation.editor.unsaved_confirm_title" + ), text: this.hass!.localize( - "ui.panel.config.automation.editor.unsaved_confirm" + "ui.panel.config.automation.editor.unsaved_confirm_text" ), confirmText: this.hass!.localize("ui.common.leave"), dismissText: this.hass!.localize("ui.common.stay"), + destructive: true, }); } return true; diff --git a/src/translations/en.json b/src/translations/en.json index 002316d2ad..3e31800936 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1836,7 +1836,8 @@ "load_error_not_deletable": "Only automations in automations.yaml can be deleted.", "load_error_unknown": "Error loading automation ({err_no}).", "save": "Save", - "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", + "unsaved_confirm_title": "Leave editor?", + "unsaved_confirm_text": "Unsaved changes will be lost.", "alias": "Name", "automation_alias": "Automation name", "automation_settings": "Automation settings", @@ -2353,7 +2354,8 @@ "load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_unknown": "Error loading scene ({err_no}).", "save": "Save", - "unsaved_confirm": "You have unsaved changes. Are you sure you want to leave?", + "unsaved_confirm_title": "Leave editor?", + "unsaved_confirm_text": "Unsaved changes will be lost.", "name": "Name", "icon": "Icon", "area": "Area", From b93f457d534a055806f2494b577e73ca202d76ea Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 12:32:17 +0200 Subject: [PATCH 55/98] Add destructive style to scene delete button (#13814) --- src/panels/config/scene/ha-scene-dashboard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 14830179ec..afebd61588 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -287,6 +287,7 @@ class HaSceneDashboard extends LitElement { confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(scene), + destructive: true, }); } From 8c125f4deee7e06b8e7855cadd69852b1dd1f9e3 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 12:33:27 +0200 Subject: [PATCH 56/98] Update Delete automation trigger/condition/action dialog (#13813) --- .../config/automation/action/ha-automation-action-row.ts | 6 +++++- .../automation/condition/ha-automation-condition-row.ts | 6 +++++- .../automation/trigger/ha-automation-trigger-row.ts | 6 +++++- src/translations/en.json | 9 ++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index c742eefc01..0d89543050 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -404,11 +404,15 @@ export default class HaAutomationActionRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.actions.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.actions.delete_confirm" + "ui.panel.config.automation.editor.actions.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index dcc93b8015..386e158d01 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -314,11 +314,15 @@ export default class HaAutomationConditionRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.conditions.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.conditions.delete_confirm" + "ui.panel.config.automation.editor.conditions.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, 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 2674664af9..8d71152164 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -430,11 +430,15 @@ export default class HaAutomationTriggerRow extends LitElement { private _onDelete() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.triggers.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.editor.triggers.delete_confirm" + "ui.panel.config.automation.editor.triggers.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), + destructive: true, confirm: () => { fireEvent(this, "value-changed", { value: null }); }, diff --git a/src/translations/en.json b/src/translations/en.json index 3e31800936..08396ef1ab 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1888,7 +1888,8 @@ "change_alias": "Rename trigger", "alias": "Trigger name", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this?", + "delete_confirm_title": "Delete trigger?", + "delete_confirm_text": "It will be permanently deleted.", "unsupported_platform": "No visual editor support for platform: {platform}", "type_select": "Trigger type", "type": { @@ -2008,7 +2009,8 @@ "change_alias": "Rename condition", "alias": "Condition name", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", + "delete_confirm_title": "Delete condition?", + "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_condition": "No visual editor support for condition: {condition}", "type_select": "Condition type", "type": { @@ -2102,7 +2104,8 @@ "disable": "Disable", "disabled": "Disabled", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", + "delete_confirm_title": "Delete action?", + "delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "unsupported_action": "No visual editor support for action: {action}", "type_select": "Action type", "type": { From fc104e72802d3479bb65391ede976f667420185c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 12:34:06 +0200 Subject: [PATCH 57/98] Fix name in confirmation delete dialog (#13815) --- src/panels/config/blueprint/ha-blueprint-overview.ts | 1 - src/panels/config/scene/ha-scene-editor.ts | 3 +-- src/panels/config/script/ha-script-picker.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index ff1280e361..3a395312af 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -333,7 +333,6 @@ class HaBlueprintOverview extends LitElement { ), text: this.hass.localize( "ui.panel.config.blueprint.overview.confirm_delete_text", - "name", { name: blueprint.name } ), confirmText: this.hass!.localize("ui.common.delete"), diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 40ccc29163..598f2e1d69 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -783,8 +783,7 @@ export class HaSceneEditor extends SubscribeMixin( ), text: this.hass!.localize( "ui.panel.config.scene.picker.delete_confirm_text", - "name", - this._config?.name + { name: this._config?.name } ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index b3b78028de..8aaa8a02f4 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -327,7 +327,6 @@ class HaScriptPicker extends LitElement { ), text: this.hass.localize( "ui.panel.config.script.editor.delete_confirm_text", - "name", { name: script.name } ), confirmText: this.hass!.localize("ui.common.delete"), From 4b8960c2366359cd52892ad9ad03649adc5cc4df Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 19 Sep 2022 14:39:05 +0200 Subject: [PATCH 58/98] Update Move datadisk (not found) dialog (#13812) --- .../config/storage/dialog-move-datadisk.ts | 89 ++++++++++--------- src/translations/en.json | 3 +- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index a005f29ebd..cfe29c95ca 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -52,7 +52,23 @@ class MoveDatadiskDialog extends LitElement { try { this._osInfo = await fetchHassioHassOsInfo(this.hass); + + const data = await listDatadisks(this.hass); + if (data.devices.length > 0) { + this._devices = data.devices; + } else { + this.closeDialog(); + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.storage.datadisk.no_devices_title" + ), + text: this.hass.localize( + "ui.panel.config.storage.datadisk.no_devices_text" + ), + }); + } } catch (err: any) { + this.closeDialog(); await showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.hardware.available_hardware.failed_to_get" @@ -60,10 +76,6 @@ class MoveDatadiskDialog extends LitElement { text: extractApiErrorMessage(err), }); } - - listDatadisks(this.hass).then((data) => { - this._devices = data.devices; - }); } public closeDialog(): void { @@ -76,9 +88,10 @@ class MoveDatadiskDialog extends LitElement { } protected render(): TemplateResult { - if (!this._hostInfo || !this._osInfo) { + if (!this._hostInfo || !this._osInfo || !this._devices) { return html``; } + return html` ${this._moving - ? html` + ? html` +

    ${this.hass.localize( "ui.panel.config.storage.datadisk.moving_desc" )} -

    ` - : html`${this._devices?.length - ? html` - ${this.hass.localize( - "ui.panel.config.storage.datadisk.description", - { - current_path: this._osInfo.data_disk, - time: calculateMoveTime(this._hostInfo), - } - )} -

    +

    + ` + : html` + ${this.hass.localize( + "ui.panel.config.storage.datadisk.description", + { + current_path: this._osInfo.data_disk, + time: calculateMoveTime(this._hostInfo), + } + )} +

    - - ${this._devices.map( - (device) => - html`${device}` - )} - - ` - : this._devices === undefined - ? this.hass.localize( - "ui.panel.config.storage.datadisk.loading_devices" - ) - : this.hass.localize( - "ui.panel.config.storage.datadisk.no_devices" - )} + + ${this._devices.map( + (device) => + html`${device}` + )} + ${this.hass.localize("ui.panel.config.storage.datadisk.move")} - `} + + `}
    `; } diff --git a/src/translations/en.json b/src/translations/en.json index 08396ef1ab..289b08e6a4 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3507,7 +3507,8 @@ "title": "Move datadisk", "description": "You are currently using ''{current_path}'' as datadisk. Moving data disks will reboot your device and it's estimated to take {time} minutes. Your Home Assistant installation will not be accessible during this period. Do not disconnect the power during the move!", "select_device": "Select new datadisk", - "no_devices": "No suitable attached devices found", + "no_devices_title": "No suitable storage found", + "no_devices_text": "There is no suitable external device found. The storage capacity of the external data disk must be larger than the storage capacity of the existing disk", "moving_desc": "Rebooting and moving datadisk. Please have patience", "moving": "Moving datadisk", "loading_devices": "Loading devices", From 04181e9c289de3088086b59e7916302c4ad8611c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 20 Sep 2022 23:44:03 +0200 Subject: [PATCH 59/98] Adapt energy to core changes (#13779) --- src/data/energy.ts | 13 ++++++++-- src/data/recorder.ts | 25 +++++++++++++++++-- .../dialog-statistics-adjust-sum.ts | 3 ++- .../energy/hui-energy-devices-graph-card.ts | 13 ++++++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 4c9f4f962d..0d73a9994a 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -20,6 +20,7 @@ import { getStatisticMetadata, Statistics, StatisticsMetaData, + StatisticsUnitConfiguration, } from "./recorder"; const energyCollectionKeys: (string | undefined)[] = []; @@ -358,12 +359,19 @@ const getEnergyData = async ( // Subtract 1 hour from start to get starting point data const startMinHour = addHours(start, -1); + const lengthUnit = hass.config.unit_system.length || ""; + const units: StatisticsUnitConfiguration = { + energy: "kWh", + volume: lengthUnit === "km" ? "m³" : "ft³", + }; + const stats = await fetchStatistics( hass!, startMinHour, end, statIDs, - period + period, + units ); let statsCompare; @@ -385,7 +393,8 @@ const getEnergyData = async ( compareStartMinHour, endCompare, statIDs, - period + period, + units ); } diff --git a/src/data/recorder.ts b/src/data/recorder.ts index 22d1557c8e..f98c5b75a8 100644 --- a/src/data/recorder.ts +++ b/src/data/recorder.ts @@ -73,6 +73,23 @@ export interface StatisticsValidationResultUnsupportedUnitMetadata { }; } +export interface StatisticsUnitConfiguration { + energy?: "Wh" | "kWh" | "MWh"; + power?: "W" | "kW"; + pressure?: + | "Pa" + | "hPa" + | "kPa" + | "bar" + | "cbar" + | "mbar" + | "inHg" + | "psi" + | "mmHg"; + temperature?: "°C" | "°F" | "K"; + volume?: "ft³" | "m³"; +} + export interface StatisticsValidationResultUnsupportedUnitState { type: "unsupported_unit_state"; data: { statistic_id: string; device_class: string; metadata_unit: string }; @@ -105,7 +122,8 @@ export const fetchStatistics = ( startTime: Date, endTime?: Date, statistic_ids?: string[], - period: "5minute" | "hour" | "day" | "month" = "hour" + period: "5minute" | "hour" | "day" | "month" = "hour", + units?: StatisticsUnitConfiguration ) => hass.callWS({ type: "recorder/statistics_during_period", @@ -113,6 +131,7 @@ export const fetchStatistics = ( end_time: endTime?.toISOString(), statistic_ids, period, + units, }); export const validateStatistics = (hass: HomeAssistant) => @@ -204,13 +223,15 @@ export const adjustStatisticsSum = ( hass: HomeAssistant, statistic_id: string, start_time: string, - adjustment: number + adjustment: number, + display_unit: string ): Promise => hass.callWS({ type: "recorder/adjust_sum_statistics", statistic_id, start_time, adjustment, + display_unit, }); export const getStatisticLabel = ( diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts index b9c54f3f6a..8598bf54f1 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts +++ b/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -305,7 +305,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { this.hass, this._params!.statistic.statistic_id, this._chosenStat!.start, - this._amount! - this._origAmount! + this._amount! - this._origAmount!, + this._params!.statistic.display_unit_of_measurement ); } catch (err: any) { this._busy = false; diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 10cab6c299..a83e9ca54c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -27,6 +27,7 @@ import { fetchStatistics, getStatisticLabel, Statistics, + StatisticsUnitConfiguration, } from "../../../../data/recorder"; import { FrontendLocaleData } from "../../../../data/translation"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; @@ -181,12 +182,19 @@ export class HuiEnergyDevicesGraphCard const startMinHour = addHours(energyData.start, -1); + const lengthUnit = this.hass.config.unit_system.length || ""; + const units: StatisticsUnitConfiguration = { + energy: "kWh", + volume: lengthUnit === "km" ? "m³" : "ft³", + }; + const data = await fetchStatistics( this.hass, startMinHour, energyData.end, devices, - period + period, + units ); Object.values(data).forEach((stat) => { @@ -211,7 +219,8 @@ export class HuiEnergyDevicesGraphCard startCompareMinHour, energyData.endCompare, devices, - period + period, + units ); Object.values(compareData).forEach((stat) => { From 82a641a200805f8ea73969c3a83b710f7eb9a97c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 Sep 2022 11:15:57 +0200 Subject: [PATCH 60/98] Fix unit columns for statistics developer tool (#13830) --- .../statistics/developer-tools-statistics.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/panels/developer-tools/statistics/developer-tools-statistics.ts b/src/panels/developer-tools/statistics/developer-tools-statistics.ts index 980fabaa52..edf957b025 100644 --- a/src/panels/developer-tools/statistics/developer-tools-statistics.ts +++ b/src/panels/developer-tools/statistics/developer-tools-statistics.ts @@ -72,8 +72,14 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) { hidden: this.narrow, width: "20%", }, - unit_of_measurement: { - title: "Unit", + display_unit_of_measurement: { + title: "Display unit", + sortable: true, + filterable: true, + width: "10%", + }, + statistics_unit_of_measurement: { + title: "Statistics unit", sortable: true, filterable: true, width: "10%", From 56c78ae1080fdd002c05ad2ee37a22baa30bc1c4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 21 Sep 2022 11:16:19 +0200 Subject: [PATCH 61/98] Fix price units for gas (#13824) --- src/components/ha-textfield.ts | 7 ++ src/data/energy.ts | 2 +- .../components/ha-energy-battery-settings.ts | 6 +- .../components/ha-energy-device-settings.ts | 4 +- .../components/ha-energy-gas-settings.ts | 12 ++-- .../components/ha-energy-grid-settings.ts | 6 +- .../components/ha-energy-solar-settings.ts | 4 +- .../components/ha-energy-validation-result.ts | 4 +- .../dialogs/dialog-energy-gas-settings.ts | 64 +++++++++++-------- .../energy/dialogs/show-dialogs-energy.ts | 4 +- src/panels/config/energy/ha-config-energy.ts | 20 +++--- 11 files changed, 79 insertions(+), 54 deletions(-) diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index 15f92ad470..5377e71e7f 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -82,6 +82,13 @@ export class HaTextField extends TextFieldBase { direction: var(--direction); } + .mdc-floating-label:not(.mdc-floating-label--float-above) { + text-overflow: ellipsis; + width: inherit; + padding-right: 30px; + box-sizing: border-box; + } + input { text-align: var(--text-field-text-align, start); } diff --git a/src/data/energy.ts b/src/data/energy.ts index 0d73a9994a..c8bb6cf55a 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -630,7 +630,7 @@ export const getEnergyGasUnitCategory = ( const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; if (statisticIdWithMeta) { return ENERGY_GAS_VOLUME_UNITS.includes( - statisticIdWithMeta.display_unit_of_measurement + statisticIdWithMeta.statistics_unit_of_measurement ) ? "volume" : "energy"; diff --git a/src/panels/config/energy/components/ha-energy-battery-settings.ts b/src/panels/config/energy/components/ha-energy-battery-settings.ts index 9a7b3c6ad1..4d7ce0101a 100644 --- a/src/panels/config/energy/components/ha-energy-battery-settings.ts +++ b/src/panels/config/energy/components/ha-energy-battery-settings.ts @@ -36,7 +36,7 @@ export class EnergyBatterySettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -104,14 +104,14 @@ export class EnergyBatterySettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} ${getStatisticLabel( this.hass, source.stat_energy_to, - this.statsMetadata[source.stat_energy_to] + this.statsMetadata?.[source.stat_energy_to] )}
    diff --git a/src/panels/config/energy/components/ha-energy-device-settings.ts b/src/panels/config/energy/components/ha-energy-device-settings.ts index 1d03ed1326..52012f4106 100644 --- a/src/panels/config/energy/components/ha-energy-device-settings.ts +++ b/src/panels/config/energy/components/ha-energy-device-settings.ts @@ -35,7 +35,7 @@ export class EnergyDeviceSettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -90,7 +90,7 @@ export class EnergyDeviceSettings extends LitElement { >${getStatisticLabel( this.hass, device.stat_consumption, - this.statsMetadata[device.stat_consumption] + this.statsMetadata?.[device.stat_consumption] )} ; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -98,7 +98,7 @@ export class EnergyGasSettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} { delete source.unit_of_measurement; await this._savePreferences({ @@ -149,11 +152,12 @@ export class EnergyGasSettings extends LitElement { ev.currentTarget.closest(".row").source; showEnergySettingsGasDialog(this, { source: { ...origSource }, - unit: getEnergyGasUnitCategory( + allowedGasUnitCategory: getEnergyGasUnitCategory( this.preferences, this.statsMetadata, origSource.stat_energy_from ), + metadata: this.statsMetadata?.[origSource.stat_energy_from], saveCallback: async (newSource) => { await this._savePreferences({ ...this.preferences, diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index e4e85d79c4..c04993b0ec 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -55,7 +55,7 @@ export class EnergyGridSettings extends LitElement { public preferences!: EnergyPreferences; @property({ attribute: false }) - public statsMetadata!: Record; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -136,7 +136,7 @@ export class EnergyGridSettings extends LitElement { >${getStatisticLabel( this.hass, flow.stat_energy_from, - this.statsMetadata[flow.stat_energy_from] + this.statsMetadata?.[flow.stat_energy_from] )} ${getStatisticLabel( this.hass, flow.stat_energy_to, - this.statsMetadata[flow.stat_energy_to] + this.statsMetadata?.[flow.stat_energy_to] )} ; + public statsMetadata?: Record; @property({ attribute: false }) public validationResult?: EnergyPreferencesValidation; @@ -106,7 +106,7 @@ export class EnergySolarSettings extends LitElement { >${getStatisticLabel( this.hass, source.stat_energy_from, - this.statsMetadata[source.stat_energy_from] + this.statsMetadata?.[source.stat_energy_from] )} ${this.info diff --git a/src/panels/config/energy/components/ha-energy-validation-result.ts b/src/panels/config/energy/components/ha-energy-validation-result.ts index 88e26b42a3..4757e1f552 100644 --- a/src/panels/config/energy/components/ha-energy-validation-result.ts +++ b/src/panels/config/energy/components/ha-energy-validation-result.ts @@ -32,9 +32,7 @@ class EnergyValidationMessage extends LitElement { > ${this.hass.localize( `ui.panel.config.energy.validation.issues.${issueType}.description`, - issueType === "entity_unexpected_unit_price" - ? { currency: this.hass.config.currency } - : undefined + { currency: this.hass.config.currency } )} ${ issueType === "recorder_untracked" diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index 16ba76d3f8..2b382a40f4 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -21,6 +21,7 @@ import "../../../../components/ha-radio"; import "../../../../components/ha-formfield"; import "../../../../components/ha-textfield"; import type { HaRadio } from "../../../../components/ha-radio"; +import { getStatisticMetadata } from "../../../../data/recorder"; @customElement("dialog-energy-gas-settings") export class DialogEnergyGasSettings @@ -35,7 +36,9 @@ export class DialogEnergyGasSettings @state() private _costs?: "no-costs" | "number" | "entity" | "statistic"; - @state() private _unit?: string; + @state() private _pickableUnit?: string; + + @state() private _pickedDisplayUnit?: string; @state() private _error?: string; @@ -46,6 +49,7 @@ export class DialogEnergyGasSettings this._source = params.source ? { ...params.source } : emptyGasEnergyPreference(); + this._pickedDisplayUnit = params.metadata?.display_unit_of_measurement; this._costs = this._source.entity_energy_price ? "entity" : this._source.number_energy_price @@ -58,7 +62,8 @@ export class DialogEnergyGasSettings public closeDialog(): void { this._params = undefined; this._source = undefined; - this._unit = undefined; + this._pickableUnit = undefined; + this._pickedDisplayUnit = undefined; this._error = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -68,13 +73,16 @@ export class DialogEnergyGasSettings return html``; } - const unit = - this._unit || - (this._params.unit === undefined - ? "m³ or kWh" - : this._params.unit === "energy" - ? "kWh" - : "m³"); + const pickableUnit = + this._pickableUnit || + (this._params.allowedGasUnitCategory === undefined + ? "ft³, m³, Wh, kWh or MWh" + : this._params.allowedGasUnitCategory === "energy" + ? "Wh, kWh or MWh" + : "ft³ or m³"); + + const externalSource = + this._source.stat_cost && this._source.stat_cost.includes(":"); return html` @@ -160,6 +168,7 @@ export class DialogEnergyGasSettings value="entity" name="costs" .checked=${this._costs === "entity"} + .disabled=${externalSource} @change=${this._handleCostChanged} > @@ -171,7 +180,7 @@ export class DialogEnergyGasSettings .value=${this._source.entity_energy_price} .label=${this.hass.localize( `ui.panel.config.energy.gas.dialog.cost_entity_input`, - { unit } + { unit: this._pickedDisplayUnit || pickableUnit } )} @value-changed=${this._priceEntityChanged} >
    ` @@ -192,14 +201,16 @@ export class DialogEnergyGasSettings ? html` ` : ""} @@ -250,18 +261,21 @@ export class DialogEnergyGasSettings }; } - private _statisticChanged(ev: CustomEvent<{ value: string }>) { + private async _statisticChanged(ev: CustomEvent<{ value: string }>) { if (ev.detail.value) { const entity = this.hass.states[ev.detail.value]; if (entity?.attributes.unit_of_measurement) { - // Wh is normalized to kWh by stats generation - this._unit = - entity.attributes.unit_of_measurement === "Wh" - ? "kWh" - : entity.attributes.unit_of_measurement; + this._pickedDisplayUnit = entity.attributes.unit_of_measurement; + } else { + this._pickedDisplayUnit = ( + await getStatisticMetadata(this.hass, [ev.detail.value]) + )[0]?.display_unit_of_measurement; } } else { - this._unit = undefined; + this._pickedDisplayUnit = undefined; + } + if (ev.detail.value.includes(":") && this._costs !== "statistic") { + this._costs = "no-costs"; } this._source = { ...this._source!, diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts index 6feb7e7078..ac27b8206b 100644 --- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts +++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts @@ -9,6 +9,7 @@ import { GasSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, } from "../../../../data/energy"; +import { StatisticsMetaData } from "../../../../data/recorder"; export interface EnergySettingsGridFlowDialogParams { source?: @@ -45,7 +46,8 @@ export interface EnergySettingsBatteryDialogParams { export interface EnergySettingsGasDialogParams { source?: GasSourceTypeEnergyPreference; - unit?: EnergyGasUnit; + allowedGasUnitCategory?: EnergyGasUnit; + metadata?: StatisticsMetaData; saveCallback: (source: GasSourceTypeEnergyPreference) => Promise; } diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index d779876137..22006e511a 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -90,37 +90,37 @@ class HaConfigEnergy extends LitElement {
    From cedde3d6a205ccbcc2fa288c3965a9bae891d744 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 21 Sep 2022 11:23:22 +0200 Subject: [PATCH 62/98] Avoid overflow with combobox (#13835) --- src/components/ha-combo-box.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ha-combo-box.ts b/src/components/ha-combo-box.ts index a23ef9ee2e..3b98083608 100644 --- a/src/components/ha-combo-box.ts +++ b/src/components/ha-combo-box.ts @@ -290,6 +290,7 @@ export class HaComboBox extends LitElement { } vaadin-combo-box-light { position: relative; + --vaadin-combo-box-overlay-max-height: calc(45vh); } ha-textfield { width: 100%; From 977fdd9fbbcd284556b7539ecd02928095ac76ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 Sep 2022 11:30:58 +0200 Subject: [PATCH 63/98] Add missing translations to statistics graph card editor (#13836) --- .../hui-statistics-graph-card-editor.ts | 18 +++++++++--------- src/translations/en.json | 10 +++++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index 0d4dd1b21d..1dd5f3d8dc 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -65,12 +65,6 @@ const cardConfigStruct = assign( const periods = ["5minute", "hour", "day", "month"] as const; const stat_types = ["mean", "min", "max", "sum"] as const; -const stat_type_labels = { - mean: "Mean", - min: "Min", - max: "Max", - sum: "Sum", -} as const; @customElement("hui-statistics-graph-card-editor") export class HuiStatisticsGraphCardEditor @@ -156,7 +150,9 @@ export class HuiStatisticsGraphCardEditor multiple: true, options: stat_types.map((stat_type) => ({ value: stat_type, - label: stat_type_labels[stat_type], + label: localize( + `ui.panel.lovelace.editor.card.statistics-graph.stat_type_labels.${stat_type}` + ), disabled: !metaDatas || !metaDatas?.every((metaData) => @@ -214,8 +210,12 @@ export class HuiStatisticsGraphCardEditor > Date: Wed, 21 Sep 2022 14:42:51 +0200 Subject: [PATCH 64/98] Add navigation picker for dashboards (#13826) * Add navigation picker for dashboards * Rename to navigation * Fix empty title and path * Use hass panels instead of fetching dashboards * Apply suggestions --- src/common/string/title-case.ts | 4 + src/components/ha-navigation-picker.ts | 221 ++++++++++++++++++ .../ha-selector/ha-selector-navigation.ts | 47 ++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 6 + .../lovelace/components/hui-action-editor.ts | 21 +- .../config-elements/hui-area-card-editor.ts | 6 +- 7 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 src/common/string/title-case.ts create mode 100644 src/components/ha-navigation-picker.ts create mode 100644 src/components/ha-selector/ha-selector-navigation.ts diff --git a/src/common/string/title-case.ts b/src/common/string/title-case.ts new file mode 100644 index 0000000000..f089f6520c --- /dev/null +++ b/src/common/string/title-case.ts @@ -0,0 +1,4 @@ +export const titleCase = (s) => + s.replace(/^_*(.)|_+(.)/g, (_s, c, d) => + c ? c.toUpperCase() : " " + d.toUpperCase() + ); diff --git a/src/components/ha-navigation-picker.ts b/src/components/ha-navigation-picker.ts new file mode 100644 index 0000000000..84c1084b30 --- /dev/null +++ b/src/components/ha-navigation-picker.ts @@ -0,0 +1,221 @@ +import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { titleCase } from "../common/string/title-case"; +import { + fetchConfig, + LovelaceConfig, + LovelaceViewConfig, +} from "../data/lovelace"; +import { PolymerChangedEvent } from "../polymer-types"; +import { HomeAssistant, PanelInfo } from "../types"; +import "./ha-combo-box"; +import type { HaComboBox } from "./ha-combo-box"; +import "./ha-icon"; + +type NavigationItem = { + path: string; + icon: string; + title: string; +}; + +const DEFAULT_ITEMS: NavigationItem[] = [{ path: "", icon: "", title: "" }]; + +// eslint-disable-next-line lit/prefer-static-styles +const rowRenderer: ComboBoxLitRenderer = (item) => html` + + + ${item.title || item.path} + ${item.path} + +`; + +const createViewNavigationItem = ( + prefix: string, + view: LovelaceViewConfig, + index: number +) => ({ + path: `/${prefix}/${view.path ?? index}`, + icon: view.icon ?? "mdi:view-compact", + title: view.title ?? (view.path ? titleCase(view.path) : `${index}`), +}); + +const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({ + path: `/${panel.url_path}`, + icon: panel.icon ?? "mdi:view-dashboard", + title: + panel.url_path === hass.defaultPanel + ? hass.localize("panel.states") + : hass.localize(`panel.${panel.title}`) || + panel.title || + (panel.url_path ? titleCase(panel.url_path) : ""), +}); + +@customElement("ha-navigation-picker") +export class HaNavigationPicker extends LitElement { + @property() public hass?: HomeAssistant; + + @property() public label?: string; + + @property() public value?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @state() private _opened = false; + + private navigationItemsLoaded = false; + + private navigationItems: NavigationItem[] = DEFAULT_ITEMS; + + @query("ha-combo-box", true) private comboBox!: HaComboBox; + + protected render(): TemplateResult { + return html` + + + `; + } + + private async _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + if (this._opened && !this.navigationItemsLoaded) { + this._loadNavigationItems(); + } + } + + private async _loadNavigationItems() { + this.navigationItemsLoaded = true; + + const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({ + id, + ...panel, + })); + const lovelacePanels = panels.filter( + (panel) => panel.component_name === "lovelace" + ); + + const viewConfigs = await Promise.all( + lovelacePanels.map((panel) => + fetchConfig( + this.hass!.connection, + // path should be null to fetch default lovelace panel + panel.url_path === "lovelace" ? null : panel.url_path, + true + ) + .then((config) => [panel.id, config] as [string, LovelaceConfig]) + .catch((_) => [panel.id, undefined] as [string, undefined]) + ) + ); + + const panelViewConfig = new Map(viewConfigs); + + this.navigationItems = []; + + for (const panel of panels) { + this.navigationItems.push(createPanelNavigationItem(this.hass!, panel)); + + const config = panelViewConfig.get(panel.id); + + if (!config) continue; + + config.views.forEach((view, index) => + this.navigationItems.push( + createViewNavigationItem(panel.url_path, view, index) + ) + ); + } + + this.comboBox.filteredItems = this.navigationItems; + } + + protected shouldUpdate(changedProps: PropertyValues) { + return !this._opened || changedProps.has("_opened"); + } + + private _valueChanged(ev: PolymerChangedEvent) { + ev.stopPropagation(); + this._setValue(ev.detail.value); + } + + private _setValue(value: string) { + this.value = value; + fireEvent( + this, + "value-changed", + { value: this._value }, + { + bubbles: false, + composed: false, + } + ); + } + + private _filterChanged(ev: CustomEvent): void { + const filterString = ev.detail.value.toLowerCase(); + const characterCount = filterString.length; + if (characterCount >= 2) { + const filteredItems: NavigationItem[] = []; + + this.navigationItems.forEach((item) => { + if ( + item.path.toLowerCase().includes(filterString) || + item.title.toLowerCase().includes(filterString) + ) { + filteredItems.push(item); + } + }); + + if (filteredItems.length > 0) { + this.comboBox.filteredItems = filteredItems; + } else { + this.comboBox.filteredItems = []; + } + } else { + this.comboBox.filteredItems = this.navigationItems; + } + } + + private get _value() { + return this.value || ""; + } + + static get styles() { + return css` + ha-icon, + ha-svg-icon { + color: var(--primary-text-color); + position: relative; + bottom: 0px; + } + *[slot="prefix"] { + margin-right: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-navigation-picker": HaNavigationPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-navigation.ts b/src/components/ha-selector/ha-selector-navigation.ts new file mode 100644 index 0000000000..e275c47d3a --- /dev/null +++ b/src/components/ha-selector/ha-selector-navigation.ts @@ -0,0 +1,47 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { NavigationSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-navigation-picker"; + +@customElement("ha-selector-navigation") +export class HaNavigationSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: NavigationSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-navigation": HaNavigationSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 40f22e1a13..bc177836f5 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -16,6 +16,7 @@ import "./ha-selector-device"; import "./ha-selector-duration"; import "./ha-selector-entity"; import "./ha-selector-file"; +import "./ha-selector-navigation"; import "./ha-selector-number"; import "./ha-selector-object"; import "./ha-selector-select"; diff --git a/src/data/selector.ts b/src/data/selector.ts index 9b1e570fb4..c82a8e3a06 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -21,6 +21,7 @@ export type Selector = | IconSelector | LocationSelector | MediaSelector + | NavigationSelector | NumberSelector | ObjectSelector | SelectSelector @@ -171,6 +172,11 @@ export interface MediaSelectorValue { }; } +export interface NavigationSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + navigation: {}; +} + export interface NumberSelector { number: { min?: number; diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 0874c7e3b8..86d98726e4 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -14,6 +14,7 @@ import { import { ServiceAction } from "../../../data/script"; import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; +import "../../../components/ha-navigation-picker"; @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @@ -89,14 +90,14 @@ export class HuiActionEditor extends LitElement {
    ${this.config?.action === "navigate" ? html` - + @value-changed=${this._navigateValueChanged} + > ` : ""} ${this.config?.action === "url" @@ -193,6 +194,16 @@ export class HuiActionEditor extends LitElement { fireEvent(this, "value-changed", { value }); } + private _navigateValueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = { + ...this.config!, + navigation_path: ev.detail.value, + }; + + fireEvent(this, "value-changed", { value }); + } + static get styles(): CSSResultGroup { return css` .dropdown { diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts index c1731bf95f..16c00d9224 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -26,7 +26,11 @@ const SCHEMA = [ name: "", type: "grid", schema: [ - { name: "navigation_path", required: false, selector: { text: {} } }, + { + name: "navigation_path", + required: false, + selector: { navigation: {} }, + }, { name: "theme", required: false, selector: { theme: {} } }, ], }, From 5a150ac80dfb7014715bf901f9a8b4b3f9229682 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 22 Sep 2022 01:47:08 -0700 Subject: [PATCH 65/98] Prompt user to remove application credentials when deleting the integration configuration (#13159) Co-authored-by: Bram Kragten --- src/data/application_credential.ts | 13 ++++ .../integrations/ha-integration-card.ts | 72 +++++++++++++++++++ src/translations/en.json | 8 +++ 3 files changed, 93 insertions(+) diff --git a/src/data/application_credential.ts b/src/data/application_credential.ts index 5dee1bd355..65f9d25a7d 100644 --- a/src/data/application_credential.ts +++ b/src/data/application_credential.ts @@ -8,6 +8,10 @@ export interface ApplicationCredentialsConfig { integrations: Record; } +export interface ApplicationCredentialsConfigEntry { + application_credentials_id?: string; +} + export interface ApplicationCredential { id: string; domain: string; @@ -21,6 +25,15 @@ export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) => type: "application_credentials/config", }); +export const fetchApplicationCredentialsConfigEntry = async ( + hass: HomeAssistant, + configEntryId: string +) => + hass.callWS({ + type: "application_credentials/config_entry", + config_entry_id: configEntryId, + }); + export const fetchApplicationCredentials = async (hass: HomeAssistant) => hass.callWS({ type: "application_credentials/list", diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index 907ea34a68..b1bdf91318 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -31,6 +31,10 @@ import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-svg-icon"; +import { + fetchApplicationCredentialsConfigEntry, + deleteApplicationCredential, +} from "../../../data/application_credential"; import { getSignedPath } from "../../../data/auth"; import { ConfigEntry, @@ -698,6 +702,10 @@ export class HaIntegrationCard extends LitElement { private async _removeIntegration(configEntry: ConfigEntry) { const entryId = configEntry.entry_id; + const applicationCredentialsId = await this._applicationCredentialForRemove( + entryId + ); + const confirmed = await showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.integrations.config_entry.delete_confirm_title", @@ -723,6 +731,70 @@ export class HaIntegrationCard extends LitElement { ), }); } + if (applicationCredentialsId) { + this._removeApplicationCredential(applicationCredentialsId); + } + } + + // Return an application credentials id for this config entry to prompt the + // user for removal. This is best effort so we don't stop overall removal + // if the integration isn't loaded or there is some other error. + private async _applicationCredentialForRemove(entryId: string) { + try { + return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId)) + .application_credentials_id; + } catch (err: any) { + // We won't prompt the user to remove credentials + return null; + } + } + + private async _removeApplicationCredential(applicationCredentialsId: string) { + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_title" + ), + text: html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_prompt" + )}, +
    +
    + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_detail" + )} +
    +
    +
    + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.learn_more" + )} + `, + destructive: true, + confirmText: this.hass.localize("ui.common.remove"), + dismissText: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.dismiss" + ), + }); + if (!confirmed) { + return; + } + try { + await deleteApplicationCredential(this.hass, applicationCredentialsId); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_entry.application_credentials.delete_error_title" + ), + text: err.message, + }); + } } private async _reloadIntegration(configEntry: ConfigEntry) { diff --git a/src/translations/en.json b/src/translations/en.json index 380558dbc9..45cbcfcaba 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2855,6 +2855,14 @@ "stop_ignore": "Stop ignoring" }, "config_entry": { + "application_credentials": { + "delete_title": "Application Credentials", + "delete_prompt": "Would you like to also remove Application Credentials for this integration?", + "delete_detail": "If you remove them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be acccessed from the Application Credentials menu.", + "delete_error_title": "Removing Application Credential failed", + "dismiss": "Keep", + "learn_more": "Learn more about Application Credentials" + }, "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", "entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}", "services": "{count} {count, plural,\n one {service}\n other {services}\n}", From 8e245c8a8316c856cbce1e3c3774e474ed5094a1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 22 Sep 2022 15:19:34 +0200 Subject: [PATCH 66/98] Fix move data disk dialog (#13845) --- src/panels/config/storage/dialog-move-datadisk.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index cfe29c95ca..40a55395f0 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -130,13 +130,18 @@ class MoveDatadiskDialog extends LitElement { @selected=${this._select_device} @closed=${stopPropagation} dialogInitialFocus + fixedMenuPosition > ${this._devices.map( (device) => - html`${device}` + html` + ${device} + ` )} + Test + Test + Test + Test Date: Thu, 22 Sep 2022 15:19:59 +0200 Subject: [PATCH 67/98] Fix select issues (#13839) --- src/components/ha-selector/ha-selector-select.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index d051b5f125..c454d339cd 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -41,7 +41,7 @@ export class HaSelectSelector extends LitElement { ); if (!this.selector.select.custom_value && this._mode === "list") { - if (!this.selector.select.multiple || this.required) { + if (!this.selector.select.multiple) { return html`
    ${this.label} @@ -64,7 +64,8 @@ export class HaSelectSelector extends LitElement { return html`
    - ${this.label}${options.map( + ${this.label} + ${options.map( (item: SelectOption) => html` !item.disabled && !this.value?.includes(item.value) + (option) => !option.disabled && !value?.includes(option.value) )} @filter-changed=${this._filterChanged} @value-changed=${this._comboBoxValueChanged} @@ -290,6 +291,9 @@ export class HaSelectSelector extends LitElement { ha-formfield { display: block; } + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } `; } From 9f19bdde6591f9b6a6050f6aa333c4b8b29293a1 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 22 Sep 2022 15:21:42 +0200 Subject: [PATCH 68/98] Allow proxy in codespaces env for core (#13842) --- script/core | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/script/core b/script/core index 8ac6da6284..93b7021d57 100755 --- a/script/core +++ b/script/core @@ -46,6 +46,14 @@ frontend: # development_repo: ${WD}" >> "${WD}/config/configuration.yaml" fi + if [ ! -z "${CODESPACES}" ]; then + echo " +http: + use_x_forwarded_for: true + trusted_proxies: + - 127.0.0.1 +" >> "${WD}/config/configuration.yaml" + fi fi hass -c "${WD}/config" From db8bc9d34a4d35582dd5cff38638e07a20a2b1bd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 22 Sep 2022 15:22:20 +0200 Subject: [PATCH 69/98] Update delete automation dialog (#13765) --- src/panels/config/automation/ha-automation-editor.ts | 7 ++++++- src/panels/config/automation/ha-automation-picker.ts | 7 ++++++- src/translations/en.json | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 627a052450..e7c7236fd3 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -575,10 +575,15 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { private async _deleteConfirm() { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: this._config?.alias } ), confirmText: this.hass!.localize("ui.common.delete"), + destructive: true, dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(), }); diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index dc395aefaf..3fe409f34b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -341,12 +341,17 @@ class HaAutomationPicker extends LitElement { private async _deleteConfirm(automation) { showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.delete_confirm_title" + ), text: this.hass.localize( - "ui.panel.config.automation.picker.delete_confirm" + "ui.panel.config.automation.picker.delete_confirm_text", + { name: automation.name } ), confirmText: this.hass!.localize("ui.common.delete"), dismissText: this.hass!.localize("ui.common.cancel"), confirm: () => this._delete(automation), + destructive: true, }); } diff --git a/src/translations/en.json b/src/translations/en.json index 45cbcfcaba..2629fdf097 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1796,7 +1796,8 @@ "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", "delete": "[%key:ui::common::delete%]", - "delete_confirm": "Are you sure you want to delete this automation?", + "delete_confirm_title": "Delete automation?", + "delete_confirm_text": "{name} will be permanently deleted.", "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", "headers": { From 5422fda99099be1fb66d5458292c7b6edd352fec Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 22 Sep 2022 15:24:15 +0200 Subject: [PATCH 70/98] Update import blueprint dialog (#13840) --- .../blueprint/dialog-import-blueprint.ts | 95 ++++++++++++------- src/translations/en.json | 10 +- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index ef43378c16..1b97d52e8d 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -1,4 +1,5 @@ import "@material/mwc-button"; +import { mdiOpenInNew } from "@mdi/js"; import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -105,50 +106,61 @@ class DialogImportBlueprint extends LitElement { >
    ${this._result.raw_data}
    ` - : html`${this.hass.localize( - "ui.panel.config.blueprint.add.import_introduction_link", - "community_link", - html`${this.hass.localize( - "ui.panel.config.blueprint.add.community_forums" - )}` - )} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_introduction" + )} +

    + + ${this.hass.localize( + "ui.panel.config.blueprint.add.community_forums" + )} + + + `} + >
    + `}
    + + ${this.hass.localize("ui.common.cancel")} + ${!this._result - ? html` - ${this._importing - ? html`` - : ""} - ${this.hass.localize("ui.panel.config.blueprint.add.import_btn")} - ` - : html` - ${this.hass.localize("ui.common.cancel")} + ${this._importing + ? html`` + : ""} + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_btn" + )} + ` + : html` ` : ""} ${this.hass.localize("ui.panel.config.blueprint.add.save_btn")} - `} + + `} `; } @@ -215,9 +228,19 @@ class DialogImportBlueprint extends LitElement { static styles = [ haStyleDialog, css` + p { + margin-top: 0; + margin-bottom: 8px; + } ha-textfield { display: block; - margin-top: 8px; + margin-top: 24px; + } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index 2629fdf097..2200bf2228 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2258,15 +2258,15 @@ "add": { "header": "Import a blueprint", "import_header": "Blueprint ''{name}''", - "import_introduction_link": "You can import blueprints of other users from Github and the {community_link}. Enter the URL of the blueprint below.", - "community_forums": "community forums", - "url": "URL of the blueprint", + "import_introduction": "Import blueprints of other users from GitHub and the community forums by pasting the address below.", + "community_forums": "View blueprints on the community forums", + "url": "Blueprint address", "raw_blueprint": "Blueprint content", "importing": "Loading blueprint…", - "import_btn": "Preview blueprint", + "import_btn": "Preview", "saving": "Importing blueprint…", "save_btn": "Import blueprint", - "error_no_url": "Please enter the URL of the blueprint.", + "error_no_url": "Please enter the blueprint address.", "unsupported_blueprint": "This blueprint is not supported", "file_name": "Blueprint Path" } From 6252955bb5cee4d9c28b76149814186f0b9ba728 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 Sep 2022 09:03:17 +0200 Subject: [PATCH 71/98] Simplify energy settings (#13846) --- demo/src/stubs/energy.ts | 5 ----- src/data/energy.ts | 8 +------- .../config/energy/dialogs/dialog-energy-gas-settings.ts | 1 - .../energy/dialogs/dialog-energy-grid-flow-settings.ts | 3 --- 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts index 00e7ef7c3f..80f10b968c 100644 --- a/demo/src/stubs/energy.ts +++ b/demo/src/stubs/energy.ts @@ -11,14 +11,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_from: "sensor.energy_consumption_tarif_1", stat_cost: "sensor.energy_consumption_tarif_1_cost", - entity_energy_from: "sensor.energy_consumption_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_from: "sensor.energy_consumption_tarif_2", stat_cost: "sensor.energy_consumption_tarif_2_cost", - entity_energy_from: "sensor.energy_consumption_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -27,14 +25,12 @@ export const mockEnergy = (hass: MockHomeAssistant) => { { stat_energy_to: "sensor.energy_production_tarif_1", stat_compensation: "sensor.energy_production_tarif_1_compensation", - entity_energy_to: "sensor.energy_production_tarif_1", entity_energy_price: null, number_energy_price: null, }, { stat_energy_to: "sensor.energy_production_tarif_2", stat_compensation: "sensor.energy_production_tarif_2_compensation", - entity_energy_to: "sensor.energy_production_tarif_2", entity_energy_price: null, number_energy_price: null, }, @@ -55,7 +51,6 @@ export const mockEnergy = (hass: MockHomeAssistant) => { type: "gas", stat_energy_from: "sensor.energy_gas", stat_cost: "sensor.energy_gas_cost", - entity_energy_from: "sensor.energy_gas", entity_energy_price: null, number_energy_price: null, }, diff --git a/src/data/energy.ts b/src/data/energy.ts index c8bb6cf55a..8420419978 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -29,7 +29,6 @@ export const emptyFlowFromGridSourceEnergyPreference = (): FlowFromGridSourceEnergyPreference => ({ stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -38,7 +37,6 @@ export const emptyFlowToGridSourceEnergyPreference = (): FlowToGridSourceEnergyPreference => ({ stat_energy_to: "", stat_compensation: null, - entity_energy_to: null, entity_energy_price: null, number_energy_price: null, }); @@ -68,7 +66,6 @@ export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({ type: "gas", stat_energy_from: "", stat_cost: null, - entity_energy_from: null, entity_energy_price: null, number_energy_price: null, }); @@ -93,7 +90,6 @@ export interface FlowFromGridSourceEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; } @@ -105,8 +101,7 @@ export interface FlowToGridSourceEnergyPreference { // $ meter stat_compensation: string | null; - // Can be used to generate costs if stat_cost omitted - entity_energy_to: string | null; + // Can be used to generate costs if stat_compensation omitted entity_energy_price: string | null; number_energy_price: number | null; } @@ -142,7 +137,6 @@ export interface GasSourceTypeEnergyPreference { stat_cost: string | null; // Can be used to generate costs if stat_cost omitted - entity_energy_from: string | null; entity_energy_price: string | null; number_energy_price: number | null; unit_of_measurement?: string | null; diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index 2b382a40f4..e98621f903 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -280,7 +280,6 @@ export class DialogEnergyGasSettings this._source = { ...this._source!, stat_energy_from: ev.detail.value, - entity_energy_from: ev.detail.value, }; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index a2d688f2b3..c2cab0d7b2 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -269,9 +269,6 @@ export class DialogEnergyGridFlowSettings [this._params!.direction === "from" ? "stat_energy_from" : "stat_energy_to"]: ev.detail.value, - [this._params!.direction === "from" - ? "entity_energy_from" - : "entity_energy_to"]: ev.detail.value, }; } From 51874329d1b01caa743d8db03b3ad2947af5bb57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Sep 2022 14:15:58 +0200 Subject: [PATCH 72/98] Bump actions/stale from 5.2.0 to 6.0.0 (#13874) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c55578ffec..5bbb751eb2 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 90 days stale policy - uses: actions/stale@v5.2.0 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 From c79955e76af5dd8ef2b6f1aae1db75165862ad6d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 27 Sep 2022 15:37:48 +0200 Subject: [PATCH 73/98] Add subview option to dashboard views (#13822) --- src/data/lovelace.ts | 2 + .../editor/view-editor/hui-view-editor.ts | 58 +++++++++++++++-- src/panels/lovelace/hui-root.ts | 65 +++++++++++++++---- src/translations/en.json | 6 +- 4 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 28d1c4886f..432cb6167b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -93,6 +93,8 @@ export interface LovelaceViewConfig { panel?: boolean; background?: string; visible?: boolean | ShowViewConfig[]; + subview?: boolean; + back_path?: string; } export interface LovelaceViewElement extends HTMLElement { diff --git a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts index 323e6dab96..e25345e36f 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -1,10 +1,10 @@ -import "../../../../components/ha-form/ha-form"; import { html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; import { slugify } from "../../../../common/string/slugify"; import type { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { LovelaceViewConfig } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; @@ -33,7 +33,7 @@ export class HuiViewEditor extends LitElement { private _suggestedPath = false; private _schema = memoizeOne( - (localize: LocalizeFunc) => + (localize: LocalizeFunc, subview: boolean, showAdvanced: boolean) => [ { name: "title", selector: { text: {} } }, { @@ -63,6 +63,20 @@ export class HuiViewEditor extends LitElement { }, }, }, + { + name: "subview", + selector: { + boolean: {}, + }, + }, + ...(subview && showAdvanced + ? [ + { + name: "back_path", + selector: { navigation: {} }, + }, + ] + : []), ] as const ); @@ -84,7 +98,12 @@ export class HuiViewEditor extends LitElement { return html``; } - const schema = this._schema(this.hass.localize); + const schema = this._schema( + this.hass.localize, + this._config.subview ?? false, + this.hass.userData?.showAdvanced ?? false + ); + const data = { theme: "Backend-selected", ...this._config, @@ -96,18 +115,22 @@ export class HuiViewEditor extends LitElement { .hass=${this.hass} .data=${data} .schema=${schema} - .computeLabel=${this._computeLabelCallback} + .computeLabel=${this._computeLabel} + .computeHelper=${this._computeHelper} @value-changed=${this._valueChanged} > `; } private _valueChanged(ev: CustomEvent): void { - const config = ev.detail.value; + const config = ev.detail.value as LovelaceViewConfig; if (config.type === "masonry") { delete config.type; } + if (!config.subview) { + delete config.back_path; + } if ( this.isNew && @@ -122,7 +145,7 @@ export class HuiViewEditor extends LitElement { fireEvent(this, "view-config-changed", { config }); } - private _computeLabelCallback = ( + private _computeLabel = ( schema: SchemaUnion> ) => { switch (schema.name) { @@ -130,12 +153,35 @@ export class HuiViewEditor extends LitElement { return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url"); case "type": return this.hass.localize("ui.panel.lovelace.editor.edit_view.type"); + case "subview": + return this.hass.localize("ui.panel.lovelace.editor.edit_view.subview"); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path" + ); default: return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); } }; + + private _computeHelper = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "subview": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.subview_helper" + ); + case "back_path": + return this.hass.localize( + "ui.panel.lovelace.editor.edit_view.back_path_helper" + ); + default: + return undefined; + } + }; } declare global { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 3539e6f26f..05e5187259 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -112,6 +112,11 @@ class HUIRoot extends LitElement { } protected render(): TemplateResult { + const views = this.lovelace?.config.views ?? []; + + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + return html` - - ${this.lovelace!.config.views.length > 1 + ${curViewConfig?.subview + ? html` + + ` + : html` + + `} + ${curViewConfig?.subview + ? html`
    ${curViewConfig.title}
    ` + : views.filter((view) => !view.subview).length > 1 ? html` - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` e.user === this.hass!.user!.id - )) || - view.visible === false) + view.subview || + (view.visible !== undefined && + ((Array.isArray(view.visible) && + !view.visible.some( + (e) => + e.user === this.hass!.user!.id + )) || + view.visible === false)) ), })} > @@ -473,7 +490,7 @@ class HUIRoot extends LitElement { @iron-activate=${this._handleViewSelected} dir=${computeRTLDirection(this.hass!)} > - ${this.lovelace!.config.views.map( + ${views.map( (view) => html` @@ -528,7 +548,7 @@ class HUIRoot extends LitElement { class="edit-icon view" @click=${this._moveViewRight} ?disabled=${(this._curView! as number) + 1 === - this.lovelace!.config.views.length} + views.length} > ` : ""} @@ -720,6 +740,20 @@ class HUIRoot extends LitElement { }); } + private _goBack(): void { + const views = this.lovelace?.config.views ?? []; + const curViewConfig = + typeof this._curView === "number" ? views[this._curView] : undefined; + + if (curViewConfig?.back_path) { + navigate(curViewConfig.back_path); + } else if (history.length > 0) { + history.back(); + } else { + navigate(views[0].path!); + } + } + private _handleRawEditor(ev: CustomEvent): void { if (!shouldHandleRequestSelectedEvent(ev)) { return; @@ -1019,6 +1053,9 @@ class HUIRoot extends LitElement { --mdc-button-outline-color: var(--app-header-edit-text-color, #fff); --mdc-typography-button-font-size: 14px; } + .child-view-icon { + opacity: 0.5; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 2200bf2228..e73e58b2fa 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3749,7 +3749,11 @@ "masonry": "Masonry (default)", "sidebar": "Sidebar", "panel": "Panel (1 card)" - } + }, + "subview": "Subview", + "subview_helper": "Subviews don't appear in tabs and have a back button.", + "back_path": "Back path (optional)", + "back_path_helper": "Only for subviews. If empty, clicking on back button will go to the previous page." }, "edit_badges": { "view_no_badges": "Badges are not be supported by the current view type." From 01fd2787beb020a03e6c79477c6d4a25c2d87969 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 27 Sep 2022 15:38:29 +0200 Subject: [PATCH 74/98] Update add application credentials dialog (#13879) --- .../dialog-add-application-credential.ts | 52 ++++++++++++++++--- src/translations/en.json | 10 ++-- 2 files changed, 52 insertions(+), 10 deletions(-) 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 e01ded0e87..f1fc6ecf87 100644 --- a/src/panels/config/application_credentials/dialog-add-application-credential.ts +++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts @@ -1,8 +1,9 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; +import { mdiOpenInNew } from "@mdi/js"; +import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-circular-progress"; import "../../../components/ha-combo-box"; @@ -10,14 +11,15 @@ import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-markdown"; import "../../../components/ha-textfield"; import { - fetchApplicationCredentialsConfig, - createApplicationCredential, - ApplicationCredentialsConfig, ApplicationCredential, + ApplicationCredentialsConfig, + createApplicationCredential, + fetchApplicationCredentialsConfig, } from "../../../data/application_credential"; import { domainToName } from "../../../data/integration"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential"; interface Domain { @@ -98,6 +100,25 @@ export class DialogAddApplicationCredential extends LitElement { >
    ${this._error ? html`
    ${this._error}
    ` : ""} +

    + ${this.hass.localize( + "ui.panel.config.application_credentials.editor.description" + )} +
    + + ${this.hass!.localize( + "ui.panel.config.application_credentials.editor.view_documentation" + )} + + +

    ${this._loading @@ -163,15 +192,18 @@ export class DialogAddApplicationCredential extends LitElement {
    ` : html` + + ${this.hass.localize("ui.common.cancel")} + ${this.hass.localize( - "ui.panel.config.application_credentials.editor.create" + "ui.panel.config.application_credentials.editor.add" )} `} @@ -213,7 +245,7 @@ export class DialogAddApplicationCredential extends LitElement { this.closeDialog(); } - private async _createApplicationCredential(ev) { + private async _addApplicationCredential(ev) { ev.preventDefault(); if (!this._domain || !this._clientId || !this._clientSecret) { return; @@ -260,6 +292,12 @@ export class DialogAddApplicationCredential extends LitElement { display: block; margin-bottom: 24px; } + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index e73e58b2fa..a7f7dbe100 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3014,12 +3014,16 @@ "caption": "Application Credentials", "description": "Manage the OAuth Application Credentials used by Integrations", "editor": { - "caption": "Add Application Credential", - "create": "Create", + "caption": "Add Credential", + "description": "OAuth is used to grant Home Assistant access to information on other websites without giving a passwords. This mechanism is used by companies such as Spotify, Google, Withings, Microsoft, and Twitter.", + "view_documentation": "View documentation", + "add": "Add", "domain": "Integration", "name": "Name", "client_id": "OAuth Client ID", - "client_secret": "OAuth Client Secret" + "client_id_helper": "Public identifier of the OAuth application", + "client_secret": "OAuth Client Secret", + "client_secret_helper": "Secret of the OAuth application" }, "picker": { "add_application_credential": "Add Application Credential", From cc76a6c5edf82ea6c6b504d18744095eac92f313 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 27 Sep 2022 15:39:14 +0200 Subject: [PATCH 75/98] Add unique_id to entity registry (#13886) --- demo/src/ha-demo.ts | 2 ++ gallery/src/pages/misc/integration-card.ts | 1 + src/data/entity_registry.ts | 2 +- src/panels/config/entities/ha-config-entities.ts | 4 +++- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index b583245e5a..b80c4d71de 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -68,6 +68,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "co2_intensity", }, { config_entry_id: "co2signal", @@ -82,6 +83,7 @@ class HaDemo extends HomeAssistantAppEl { hidden_by: null, entity_category: null, has_entity_name: false, + unique_id: "grid_fossil_fuel_percentage", }, ]); diff --git a/gallery/src/pages/misc/integration-card.ts b/gallery/src/pages/misc/integration-card.ts index 7d3fc2eda4..5b6b064803 100644 --- a/gallery/src/pages/misc/integration-card.ts +++ b/gallery/src/pages/misc/integration-card.ts @@ -196,6 +196,7 @@ const createEntityRegistryEntries = ( icon: null, platform: "updater", has_entity_name: false, + unique_id: "updater", }, ]; diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 78da19f6f2..075ce04b12 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -20,10 +20,10 @@ export interface EntityRegistryEntry { entity_category: "config" | "diagnostic" | null; has_entity_name: boolean; original_name?: string; + unique_id: string; } export interface ExtEntityRegistryEntry extends EntityRegistryEntry { - unique_id: string; capabilities: Record; original_icon?: string; device_class?: string; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index ac02d2e048..8e3448994f 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -68,10 +68,12 @@ import type { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; -export interface StateEntity extends Omit { +export interface StateEntity + extends Omit { readonly?: boolean; selectable?: boolean; id?: string; + unique_id?: string; } export interface EntityRow extends StateEntity { From 8d5c36a96a4c0cd5c7b726cec71d7394fcc705ac Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 27 Sep 2022 15:39:49 +0200 Subject: [PATCH 76/98] Fix more info with unnamed entity (#13859) --- src/dialogs/more-info/ha-more-info-dialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index f07a3c3863..d47a02fbad 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -90,7 +90,7 @@ export class MoreInfoDialog extends LitElement { const stateObj = this.hass.states[entityId]; const domain = computeDomain(entityId); - const name = stateObj ? computeStateName(stateObj) : entityId; + const name = (stateObj && computeStateName(stateObj)) || entityId; const tabs = this._getTabs(entityId, this.hass.user!.is_admin); return html` From 0848c096b9b7ad60dc14d88e2e76be0daf17400c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 27 Sep 2022 15:40:38 +0200 Subject: [PATCH 77/98] Fix script config form (#13878) --- src/panels/config/script/ha-script-editor.ts | 116 ++++++++++--------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 7029ba4e3f..629cff5412 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -332,7 +332,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { "yaml-mode": this._mode === "yaml", })}" > - ${this._errors ? html`
    ${this._errors}
    ` : ""} ${this._mode === "gui" ? html`
    + ${this._errors + ? html` + + ${this._errors} + + ` + : ""}
    ${this._errors} + ` + : ""} = { + alias: values.alias ?? "", + icon: values.icon, + mode: values.mode, + max: isMaxMode(values.mode) ? values.max : undefined, + }; - for (const key of Object.keys(values)) { - if (key === "sequence") { + if (!this.scriptEntityId) { + this.updateEntityId(values.id, values.alias); + } + + for (const key of Object.keys(newValues)) { + const value = newValues[key]; + + if (value === this._config![key]) { continue; } - - const value = values[key]; - - if ( - value === this._config![key] || - (key === "id" && currentId === value) - ) { - continue; - } - - changed = true; - - switch (key) { - case "id": - this._idChanged(value); - break; - case "alias": - this._aliasChanged(value); - break; - case "mode": - this._modeChanged(value); - break; - } - - if (values[key] === undefined) { + if (value === undefined) { const newConfig = { ...this._config! }; delete newConfig![key]; this._config = newConfig; } else { this._config = { ...this._config!, [key]: value }; } + changed = true; } if (changed) { @@ -638,6 +640,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _configChanged(ev) { this._config = ev.detail.value; + this._errors = undefined; this._dirty = true; } @@ -759,7 +762,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this.hass!.callApi("POST", "config/script/config/" + id, this._config).then( () => { this._dirty = false; - if (!this.scriptEntityId) { navigate(`/config/script/edit/${id}`, { replace: true }); } @@ -806,6 +808,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { max-width: 1040px; padding: 28px 20px 0; } + .config-container ha-alert { + margin-bottom: 16px; + display: block; + } ha-yaml-editor { flex-grow: 1; --code-mirror-height: 100%; From 3083d5b04ca71cf5e2c80090d9ae2018bec19dad Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 27 Sep 2022 10:02:45 -0400 Subject: [PATCH 78/98] Clean up ZHA configuration UI (#13610) --- src/data/zha.ts | 2 +- .../zha/device-actions.ts | 25 +- .../zha/ha-device-info-zha.ts | 1 + .../zha/dialog-zha-cluster.ts | 142 --------- .../zha/dialog-zha-device-zigbee-info.ts | 69 ----- .../zha/dialog-zha-manage-zigbee-device.ts | 291 ++++++++++++++++++ .../zha/dialog-zha-reconfigure-device.ts | 22 +- .../zha/show-dialog-zha-cluster.ts | 19 -- .../zha/show-dialog-zha-device-children.ts | 20 -- .../zha/show-dialog-zha-device-zigbee-info.ts | 20 -- .../show-dialog-zha-manage-zigbee-device.ts | 23 ++ .../integration-panels/zha/types.ts | 6 +- .../zha/zha-cluster-attributes.ts | 206 ++++--------- .../zha/zha-cluster-commands.ts | 198 ++++-------- .../zha/zha-clusters-data-table.ts | 2 +- .../integration-panels/zha/zha-clusters.ts | 195 ------------ .../zha/zha-device-binding.ts | 204 +++++------- ...ice-children.ts => zha-device-children.ts} | 88 ++---- .../zha/zha-device-signature.ts | 47 +++ .../zha/zha-group-binding.ts | 213 ++++--------- .../zha/zha-manage-clusters.ts | 198 ++++++++++++ src/translations/en.json | 30 +- 22 files changed, 903 insertions(+), 1118 deletions(-) delete mode 100644 src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts delete mode 100644 src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts create mode 100644 src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts delete mode 100644 src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts delete mode 100644 src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts delete mode 100644 src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts create mode 100644 src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts delete mode 100644 src/panels/config/integrations/integration-panels/zha/zha-clusters.ts rename src/panels/config/integrations/integration-panels/zha/{dialog-zha-device-children.ts => zha-device-children.ts} (52%) create mode 100644 src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts create mode 100644 src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts diff --git a/src/data/zha.ts b/src/data/zha.ts index fb0e2b0f31..55bf16d7c4 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -309,7 +309,7 @@ export const fetchCommandsForCluster = ( cluster_type: clusterType, }); -export const fetchClustersForZhaNode = ( +export const fetchClustersForZhaDevice = ( hass: HomeAssistant, ieeeAddress: string ): Promise => diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts index e9e17a3f20..907393e7c1 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/device-actions.ts @@ -1,9 +1,7 @@ import { mdiCogRefresh, mdiDelete, - mdiDrawPen, mdiFamilyTree, - mdiFileTree, mdiGroup, mdiPlus, } from "@mdi/js"; @@ -12,9 +10,7 @@ import type { DeviceRegistryEntry } from "../../../../../../data/device_registry import { fetchZHADevice } from "../../../../../../data/zha"; import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../../../types"; -import { showZHAClusterDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-cluster"; -import { showZHADeviceChildrenDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-children"; -import { showZHADeviceZigbeeInfoDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info"; +import { showZHAManageZigbeeDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device"; import { showZHAReconfigureDeviceDialog } from "../../../../integrations/integration-panels/zha/show-dialog-zha-reconfigure-device"; import type { DeviceAction } from "../../../ha-config-device-page"; @@ -59,13 +55,6 @@ export const getZHADeviceActions = async ( icon: mdiPlus, action: () => navigate(`/config/zha/add/${zhaDevice!.ieee}`), }, - { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.device_children" - ), - icon: mdiFileTree, - action: () => showZHADeviceChildrenDialog(el, { device: zhaDevice! }), - }, ] ); } @@ -73,16 +62,10 @@ export const getZHADeviceActions = async ( actions.push( ...[ { - label: hass.localize( - "ui.dialogs.zha_device_info.buttons.zigbee_information" - ), - icon: mdiDrawPen, - action: () => showZHADeviceZigbeeInfoDialog(el, { device: zhaDevice }), - }, - { - label: hass.localize("ui.dialogs.zha_device_info.buttons.clusters"), + label: hass.localize("ui.dialogs.zha_device_info.buttons.manage"), icon: mdiGroup, - action: () => showZHAClusterDialog(el, { device: zhaDevice }), + action: () => + showZHAManageZigbeeDeviceDialog(el, { device: zhaDevice }), }, { label: hass.localize("ui.dialogs.zha_device_info.buttons.view_network"), diff --git a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts index 5ccaf7c5e5..3f1f0fad16 100644 --- a/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts +++ b/src/panels/config/devices/device-detail/integration-elements/zha/ha-device-info-zha.ts @@ -23,6 +23,7 @@ export class HaDeviceActionsZha extends LitElement { @state() private _zhaDevice?: ZHADevice; protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); if (changedProperties.has("device")) { const zigbeeConnection = this.device.connections.find( (conn) => conn[0] === "zigbee" diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts deleted file mode 100644 index 84ea96c629..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-cluster.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { - Cluster, - fetchBindableDevices, - fetchGroups, - ZHADevice, - ZHAGroup, -} from "../../../../../data/zha"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { sortZHADevices, sortZHAGroups } from "./functions"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; -import { ZHAClusterSelectedParams } from "./types"; -import "./zha-cluster-attributes"; -import "./zha-cluster-commands"; -import "./zha-clusters"; -import "./zha-device-binding"; -import "./zha-group-binding"; - -@customElement("dialog-zha-cluster") -class DialogZHACluster extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _device?: ZHADevice; - - @state() private _selectedCluster?: Cluster; - - @state() private _bindableDevices: ZHADevice[] = []; - - @state() private _groups: ZHAGroup[] = []; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._device = params.device; - } - - protected updated(changedProperties: PropertyValues): void { - super.update(changedProperties); - if (changedProperties.has("_device")) { - this._fetchData(); - } - } - - protected render(): TemplateResult { - if (!this._device) { - return html``; - } - - return html` - - - ${this._selectedCluster - ? html` - - - ` - : ""} - ${this._bindableDevices.length > 0 - ? html` - - ` - : ""} - ${this._device && this._groups.length > 0 - ? html` - - ` - : ""} - - `; - } - - private _onClusterSelected( - selectedClusterEvent: HASSDomEvent - ): void { - this._selectedCluster = selectedClusterEvent.detail.cluster; - } - - private _close(): void { - this._device = undefined; - } - - private async _fetchData(): Promise { - if (this._device && this.hass) { - this._bindableDevices = - this._device && this._device.device_type !== "Coordinator" - ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( - sortZHADevices - ) - : []; - this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); - } - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-cluster": DialogZHACluster; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts deleted file mode 100644 index b9334e46d4..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceZigbeeInfoDialogParams } from "./show-dialog-zha-device-zigbee-info"; - -@customElement("dialog-zha-device-zigbee-info") -class DialogZHADeviceZigbeeInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _signature: any; - - public async showDialog( - params: ZHADeviceZigbeeInfoDialogParams - ): Promise { - this._signature = JSON.stringify( - { - ...params.device.signature, - manufacturer: params.device.manufacturer, - model: params.device.model, - class: params.device.quirk_class, - }, - null, - 2 - ); - } - - protected render(): TemplateResult { - if (!this._signature) { - return html``; - } - - return html` - - - - - `; - } - - private _close(): void { - this._signature = undefined; - } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-zha-device-zigbee-info": DialogZHADeviceZigbeeInfo; - } -} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..5ccacb9d90 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,291 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { mdiClose } from "@mdi/js"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-code-editor"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { + fetchBindableDevices, + fetchGroups, + ZHADevice, + ZHAGroup, +} from "../../../../../data/zha"; +import { haStyleDialog } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { sortZHADevices, sortZHAGroups } from "./functions"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; +import "./zha-manage-clusters"; +import "./zha-device-binding"; +import "./zha-group-binding"; +import "./zha-device-children"; +import "./zha-device-signature"; +import { + Tab, + ZHAManageZigbeeDeviceDialogParams, +} from "./show-dialog-zha-manage-zigbee-device"; +import "../../../../../components/ha-header-bar"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; + +@customElement("dialog-zha-manage-zigbee-device") +class DialogZHAManageZigbeeDevice extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public large = false; + + @state() private _currTab: Tab = "clusters"; + + @state() private _device?: ZHADevice; + + @state() private _bindableDevices: ZHADevice[] = []; + + @state() private _groups: ZHAGroup[] = []; + + public async showDialog( + params: ZHAManageZigbeeDeviceDialogParams + ): Promise { + this._device = params.device; + if (!this._device) { + this.closeDialog(); + return; + } + this._currTab = params.tab || "clusters"; + this.large = false; + } + + public closeDialog() { + this._device = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this.addEventListener("close-dialog", () => this.closeDialog()); + } + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this._device) { + return; + } + if (changedProps.has("_device")) { + const tabs = this._getTabs(this._device); + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + this._fetchData(); + } + } + + protected render(): TemplateResult { + if (!this._device) { + return html``; + } + + const tabs = this._getTabs(this._device); + + return html` + +
    + + +
    + ${this.hass.localize("ui.dialogs.zha_manage_device.heading")} +
    +
    + + ${tabs.map( + (tab) => html` + + ` + )} + +
    + +
    + ${cache( + this._currTab === "clusters" + ? html` + + ` + : this._currTab === "bindings" + ? html` + ${this._bindableDevices.length > 0 + ? html` + + ` + : ""} + ${this._device && this._groups.length > 0 + ? html` + + ` + : ""} + ` + : this._currTab === "signature" + ? html` + + ` + : html` + + ` + )} +
    +
    + `; + } + + private async _fetchData(): Promise { + if (this._device && this.hass) { + this._bindableDevices = + this._device && this._device.device_type !== "Coordinator" + ? (await fetchBindableDevices(this.hass, this._device.ieee)).sort( + sortZHADevices + ) + : []; + this._groups = (await fetchGroups(this.hass!)).sort(sortZHAGroups); + } + } + + private _enlarge() { + this.large = !this.large; + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = this._getTabs(this._device)[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _getTabs = memoizeOne((device: ZHADevice | undefined) => { + const tabs: Tab[] = ["clusters", "bindings", "signature"]; + + if ( + device && + (device.device_type === "Router" || device.device_type === "Coordinator") + ) { + tabs.push("children"); + } + + return tabs; + }); + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-surface-position: static; + --dialog-content-position: static; + --vertial-align-dialog: flex-start; + } + + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + display: block; + } + .content { + outline: none; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-header-bar { + --mdc-theme-primary: var(--app-header-background-color); + --mdc-theme-on-primary: var(--app-header-text-color, white); + border-bottom: none; + } + } + + .heading { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + @media all and (min-width: 600px) and (min-height: 501px) { + ha-dialog { + --mdc-dialog-min-width: 560px; + --mdc-dialog-max-width: 560px; + --dialog-surface-margin-top: 40px; + --mdc-dialog-max-height: calc(100% - 72px); + } + + .main-title { + overflow: hidden; + text-overflow: ellipsis; + cursor: default; + } + + :host([large]) ha-dialog, + ha-dialog[data-domain="camera"] { + --mdc-dialog-min-width: 90vw; + --mdc-dialog-max-width: 90vw; + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zha-manage-zigbee-device": DialogZHAManageZigbeeDevice; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts index 1ae26ad04d..4c43266385 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-reconfigure-device.ts @@ -12,7 +12,7 @@ import { Cluster, ClusterConfigurationEvent, ClusterConfigurationStatus, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, reconfigureNode, ZHA_CHANNEL_CFG_DONE, ZHA_CHANNEL_MSG_BIND, @@ -321,16 +321,16 @@ class DialogZHAReconfigureDevice extends LitElement { return; } this._clusterConfigurationStatuses = new Map( - (await fetchClustersForZhaNode(this.hass, this._params.device.ieee)).map( - (cluster: Cluster) => [ - cluster.id, - { - cluster: cluster, - bindSuccess: undefined, - attributes: new Map(), - }, - ] - ) + ( + await fetchClustersForZhaDevice(this.hass, this._params.device.ieee) + ).map((cluster: Cluster) => [ + cluster.id, + { + cluster: cluster, + bindSuccess: undefined, + attributes: new Map(), + }, + ]) ); this._subscribe(this._params); this._status = "started"; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts deleted file mode 100644 index 9b0c44ad5d..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-cluster.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHAClusterDialogParams { - device: ZHADevice; -} - -export const loadZHAClusterDialog = () => import("./dialog-zha-cluster"); - -export const showZHAClusterDialog = ( - element: HTMLElement, - params: ZHAClusterDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-cluster", - dialogImport: loadZHAClusterDialog, - dialogParams: params, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts deleted file mode 100644 index f8a3e64ab0..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-children.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceChildrenDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceChildrenDialog = () => - import("./dialog-zha-device-children"); - -export const showZHADeviceChildrenDialog = ( - element: HTMLElement, - zhaDeviceChildrenParams: ZHADeviceChildrenDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-children", - dialogImport: loadZHADeviceChildrenDialog, - dialogParams: zhaDeviceChildrenParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts deleted file mode 100644 index e4a2191563..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-device-zigbee-info.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { ZHADevice } from "../../../../../data/zha"; - -export interface ZHADeviceZigbeeInfoDialogParams { - device: ZHADevice; -} - -export const loadZHADeviceZigbeeInfoDialog = () => - import("./dialog-zha-device-zigbee-info"); - -export const showZHADeviceZigbeeInfoDialog = ( - element: HTMLElement, - zhaDeviceZigbeeInfoParams: ZHADeviceZigbeeInfoDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-zha-device-zigbee-info", - dialogImport: loadZHADeviceZigbeeInfoDialog, - dialogParams: zhaDeviceZigbeeInfoParams, - }); -}; diff --git a/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts new file mode 100644 index 0000000000..a2854748ff --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/show-dialog-zha-manage-zigbee-device.ts @@ -0,0 +1,23 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { ZHADevice } from "../../../../../data/zha"; + +export type Tab = "clusters" | "bindings" | "signature" | "children"; + +export interface ZHAManageZigbeeDeviceDialogParams { + device: ZHADevice; + tab?: Tab; +} + +export const loadZHAManageZigbeeDeviceDialog = () => + import("./dialog-zha-manage-zigbee-device"); + +export const showZHAManageZigbeeDeviceDialog = ( + element: HTMLElement, + params: ZHAManageZigbeeDeviceDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zha-manage-zigbee-device", + dialogImport: loadZHAManageZigbeeDeviceDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/config/integrations/integration-panels/zha/types.ts b/src/panels/config/integrations/integration-panels/zha/types.ts index dbc2954f2d..3e9af96a18 100644 --- a/src/panels/config/integrations/integration-panels/zha/types.ts +++ b/src/panels/config/integrations/integration-panels/zha/types.ts @@ -1,5 +1,5 @@ import { HaSelect } from "../../../../../components/ha-select"; -import { Cluster, ZHADevice } from "../../../../../data/zha"; +import { ZHADevice } from "../../../../../data/zha"; export interface ItemSelectedEvent { target?: HaSelect; @@ -41,10 +41,6 @@ export interface ZHADeviceSelectedParams { node: ZHADevice; } -export interface ZHAClusterSelectedParams { - cluster: Cluster; -} - export interface NodeServiceData { ieee_address: string; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts index c764a135b3..a0a3695dea 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -10,13 +8,12 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; +import "../../../../../components/buttons/ha-progress-button"; import { Attribute, Cluster, @@ -27,7 +24,7 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; +import { forwardHaptic } from "../../../../../data/haptics"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, @@ -35,18 +32,15 @@ import { SetAttributeServiceData, } from "./types"; +@customElement("zha-cluster-attributes") export class ZHAClusterAttributes extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public showHelp = false; - - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _attributes: Attribute[] = []; + @state() private _attributes: Attribute[] | undefined; @state() private _selectedAttributeId?: number; @@ -54,78 +48,52 @@ export class ZHAClusterAttributes extends LitElement { @state() private _manufacturerCodeOverride?: string | number; + @state() private _readingAttribute = false; + @state() private _setAttributeServiceData?: SetAttributeServiceData; protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._attributes = []; + this._attributes = undefined; this._selectedAttributeId = undefined; this._attributeValue = ""; this._fetchAttributesForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._attributes) { + return html``; + } return html` - -
    - - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.header" + +
    + - - -
    - - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.introduction" - )} - - - -
    - - ${this._attributes.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
    - ${this.showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_attribute_dropdown" - )} -
    + ${this._attributes.map( + (entry) => html` + + ${`${entry.name} (id: ${formatAsPaddedHex(entry.id)})`} + ` - : ""} - ${this._selectedAttributeId !== undefined - ? this._renderAttributeInteractions() - : ""} -
    - + )} + +
    + ${this._selectedAttributeId !== undefined + ? this._renderAttributeInteractions() + : ""} + `; } @@ -152,20 +120,15 @@ export class ZHAClusterAttributes extends LitElement { >
    - + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.get_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.read_zigbee_attribute" )} - - ${this.showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.help_get_zigbee_attribute" - )} -
    - ` - : ""} + ${this.hass!.localize( - "ui.panel.config.zha.cluster_attributes.set_zigbee_attribute" + "ui.panel.config.zha.cluster_attributes.write_zigbee_attribute" )} - ${this.showHelp - ? html` - - ` - : ""}
    `; } private async _fetchAttributesForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._attributes = await fetchAttributesForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._attributes.sort((a, b) => a.name.localeCompare(b.name)); + if (this._attributes.length > 0) { + this._selectedAttributeId = this._attributes[0].id; + } } } private _computeReadAttributeServiceData(): | ReadAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -224,11 +180,11 @@ export class ZHAClusterAttributes extends LitElement { private _computeSetAttributeServiceData(): | SetAttributeServiceData | undefined { - if (!this.selectedCluster || !this.selectedNode) { + if (!this.selectedCluster || !this.device) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -250,17 +206,24 @@ export class ZHAClusterAttributes extends LitElement { this._setAttributeServiceData = this._computeSetAttributeServiceData(); } - private async _onGetZigbeeAttributeClick(): Promise { + private async _onGetZigbeeAttributeClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; const data = this._computeReadAttributeServiceData(); if (data && this.hass) { - this._attributeValue = await readAttributeValue(this.hass, data); + this._readingAttribute = true; + try { + this._attributeValue = await readAttributeValue(this.hass, data); + forwardHaptic("success"); + button.actionSuccess(); + } catch (err: any) { + forwardHaptic("failure"); + button.actionError(); + } finally { + this._readingAttribute = false; + } } } - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - private _selectedAttributeChanged(event: ItemSelectedEvent): void { this._selectedAttributeId = Number(event.target!.value); this._attributeValue = ""; @@ -278,14 +241,6 @@ export class ZHAClusterAttributes extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -306,33 +261,6 @@ export class ZHAClusterAttributes extends LitElement { .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - .help-text2 { - color: grey; - padding: 16px; - } `, ]; } @@ -343,5 +271,3 @@ declare global { "zha-cluster-attributes": ZHAClusterAttributes; } } - -customElements.define("zha-cluster-attributes", ZHAClusterAttributes); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts index ca3a29be5f..129e42e74f 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts @@ -1,5 +1,4 @@ import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import "@polymer/paper-input/paper-input"; import { css, @@ -13,9 +12,7 @@ import { property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { Cluster, Command, @@ -24,7 +21,6 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { formatAsPaddedHex } from "./functions"; import { ChangeEvent, IssueCommandServiceData } from "./types"; @@ -33,13 +29,11 @@ export class ZHAClusterCommands extends LitElement { @property() public isWide?: boolean; - @property() public selectedNode?: ZHADevice; + @property() public device?: ZHADevice; @property() public selectedCluster?: Cluster; - @state() private _showHelp = false; - - @state() private _commands: Command[] = []; + @state() private _commands: Command[] | undefined; @state() private _selectedCommandId?: number; @@ -50,132 +44,97 @@ export class ZHAClusterCommands extends LitElement { protected updated(changedProperties: PropertyValues): void { if (changedProperties.has("selectedCluster")) { - this._commands = []; + this._commands = undefined; this._selectedCommandId = undefined; this._fetchCommandsForCluster(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { + if (!this.device || !this.selectedCluster || !this._commands) { + return html``; + } return html` - -
    - - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.header" + +
    + - - + ${this._commands.map( + (entry) => html` + + ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} + + ` + )} +
    - - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.introduction" - )} - - - -
    - - ${this._commands.map( - (entry) => html` - - ${entry.name + " (id: " + formatAsPaddedHex(entry.id) + ")"} - - ` - )} - -
    - ${this._showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.help_command_dropdown" + ${this._selectedCommandId !== undefined + ? html` +
    + - ` - : ""} - ${this._selectedCommandId !== undefined - ? html` -
    - -
    -
    - - ${this.hass!.localize( - "ui.panel.config.zha.cluster_commands.issue_zigbee_command" - )} - - ${this._showHelp - ? html` - - ` - : ""} -
    - ` - : ""} - - + type="number" + .value=${this._manufacturerCodeOverride} + @value-changed=${this._onManufacturerCodeOverrideChanged} + placeholder=${this.hass!.localize( + "ui.panel.config.zha.common.value" + )} + >
    +
    +
    + + ${this.hass!.localize( + "ui.panel.config.zha.cluster_commands.issue_zigbee_command" + )} + +
    + ` + : ""} + `; } private async _fetchCommandsForCluster(): Promise { - if (this.selectedNode && this.selectedCluster && this.hass) { + if (this.device && this.selectedCluster && this.hass) { this._commands = await fetchCommandsForCluster( this.hass, - this.selectedNode!.ieee, + this.device!.ieee, this.selectedCluster!.endpoint_id, this.selectedCluster!.id, this.selectedCluster!.type ); this._commands.sort((a, b) => a.name.localeCompare(b.name)); + if (this._commands.length > 0) { + this._selectedCommandId = this._commands[0].id; + } } } private _computeIssueClusterCommandServiceData(): | IssueCommandServiceData | undefined { - if (!this.selectedNode || !this.selectedCluster) { + if (!this.device || !this.selectedCluster || !this._commands) { return undefined; } return { - ieee: this.selectedNode!.ieee, + ieee: this.device!.ieee, endpoint_id: this.selectedCluster!.endpoint_id, cluster_id: this.selectedCluster!.id, cluster_type: this.selectedCluster!.type, @@ -192,10 +151,6 @@ export class ZHAClusterCommands extends LitElement { this._computeIssueClusterCommandServiceData(); } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - private _selectedCommandChanged(event): void { this._selectedCommandId = Number(event.target.value); this._issueClusterCommandServiceData = @@ -213,14 +168,6 @@ export class ZHAClusterCommands extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - .card-actions.warning ha-call-service-button { color: var(--error-color); } @@ -238,18 +185,6 @@ export class ZHAClusterCommands extends LitElement { padding-bottom: 10px; } - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - - .help-text2 { - color: grey; - padding: 16px; - } - .header { flex-grow: 1; } @@ -261,15 +196,6 @@ export class ZHAClusterCommands extends LitElement { padding-right: 0px; color: var(--primary-color); } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts index f4f0e5fdbf..9cebc071b2 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-clusters-data-table.ts @@ -59,7 +59,7 @@ export class ZHAClustersDataTable extends LitElement { title: "ID", template: (id: number) => html` ${formatAsPaddedHex(id)} `, sortable: true, - width: "15%", + width: "25%", }, endpoint_id: { title: "Endpoint ID", diff --git a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts deleted file mode 100644 index 19f2d270d2..0000000000 --- a/src/panels/config/integrations/integration-panels/zha/zha-clusters.ts +++ /dev/null @@ -1,195 +0,0 @@ -import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { property, state } from "lit/decorators"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; -import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; -import { - Cluster, - fetchClustersForZhaNode, - ZHADevice, -} from "../../../../../data/zha"; -import { haStyle } from "../../../../../resources/styles"; -import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; -import { computeClusterKey } from "./functions"; - -declare global { - // for fire event - interface HASSDomEvents { - "zha-cluster-selected": { - cluster?: Cluster; - }; - } -} - -export class ZHAClusters extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @property() public showHelp = false; - - @state() private _selectedClusterIndex = -1; - - @state() private _clusters: Cluster[] = []; - - protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { - this._clusters = []; - this._selectedClusterIndex = -1; - fireEvent(this, "zha-cluster-selected", { - cluster: undefined, - }); - this._fetchClustersForZhaNode(); - } - super.update(changedProperties); - } - - protected render(): TemplateResult { - return html` - -
    - - -
    - - ${this.hass!.localize("ui.panel.config.zha.clusters.introduction")} - - - -
    - - ${this._clusters.map( - (entry, idx) => html` - ${computeClusterKey(entry)} - ` - )} - -
    - ${this.showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.clusters.help_cluster_dropdown" - )} -
    - ` - : ""} -
    -
    - `; - } - - private async _fetchClustersForZhaNode(): Promise { - if (this.hass) { - this._clusters = await fetchClustersForZhaNode( - this.hass, - this.selectedDevice!.ieee - ); - this._clusters.sort((a, b) => a.name.localeCompare(b.name)); - } - } - - private _selectedClusterChanged(event): void { - this._selectedClusterIndex = Number(event.target!.value); - fireEvent(this, "zha-cluster-selected", { - cluster: this._clusters[this._selectedClusterIndex], - }); - } - - private _onHelpTap(): void { - this.showHelp = !this.showHelp; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - ha-select { - margin-top: 16px; - } - .menu { - width: 100%; - } - - .content { - margin-top: 24px; - } - - .header { - flex-grow: 1; - } - - ha-card { - max-width: 680px; - } - - .node-picker { - align-items: center; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - [hidden] { - display: none; - } - - .help-text { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 16px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "zha-cluster": ZHAClusters; - } -} - -customElements.define("zha-clusters", ZHAClusters); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts index 1445a572d5..993aaad79e 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts @@ -1,6 +1,4 @@ -import "@material/mwc-button/mwc-button"; import "@material/mwc-list/mwc-list-item"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,26 +9,19 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-service-description"; import { bindDevices, unbindDevices, ZHADevice } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; @customElement("zha-device-binding-control") export class ZHADeviceBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -38,77 +29,58 @@ export class ZHADeviceBindingControl extends LitElement { @state() private _deviceToBind?: ZHADevice; + @state() private _bindingOperationInProgress = false; + protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
    - Device Binding - +
    + - -
    - Bind and unbind devices. - - -
    - - ${this.bindableDevices.map( - (device, idx) => html` - - ${device.user_given_name - ? device.user_given_name - : device.name} - - ` - )} - -
    - ${this._showHelp - ? html` -
    - Select a device to issue a bind command. -
    + ${this.bindableDevices.map( + (device, idx) => html` + + ${device.user_given_name + ? device.user_given_name + : device.name} + ` - : ""} -
    - Bind - ${this._showHelp - ? html`
    Bind devices.
    ` - : ""} - Unbind - ${this._showHelp - ? html`
    Unbind devices.
    ` - : ""} -
    -
    - + )} + +
    +
    + + ${this.hass!.localize("ui.panel.config.zha.device_binding.bind")} + + + ${this.hass!.localize("ui.panel.config.zha.device_binding.unbind")} + +
    + `; } @@ -120,27 +92,41 @@ export class ZHADeviceBindingControl extends LitElement { : this.bindableDevices[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await bindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onBindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDevices(this.hass, this.device.ieee, this._deviceToBind.ieee); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindDevicesClick(): Promise { - if (this.hass && this._deviceToBind && this.selectedDevice) { - await unbindDevices( - this.hass, - this.selectedDevice.ieee, - this._deviceToBind.ieee - ); + private async _onUnbindDevicesClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + if (this.hass && this._deviceToBind && this.device) { + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDevices( + this.hass, + this.device.ieee, + this._deviceToBind.ieee + ); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -152,18 +138,6 @@ export class ZHADeviceBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -171,33 +145,9 @@ export class ZHADeviceBindingControl extends LitElement { padding-bottom: 10px; } - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - .header { flex-grow: 1; } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts similarity index 52% rename from src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts rename to src/panels/config/integrations/integration-panels/zha/zha-device-children.ts index 91bd86246c..52a1bdf1b3 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-device-children.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-children.ts @@ -1,12 +1,9 @@ -import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import memoizeOne from "memoize-one"; import { customElement, property, state } from "lit/decorators"; import { computeRTLDirection } from "../../../../../common/util/compute_rtl"; import "../../../../../components/ha-code-editor"; -import { createCloseHeading } from "../../../../../components/ha-dialog"; -import { haStyleDialog } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import { ZHADeviceChildrenDialogParams } from "./show-dialog-zha-device-children"; import "../../../../../components/data-table/ha-data-table"; import type { DataTableColumnContainer, @@ -14,7 +11,6 @@ import type { } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-circular-progress"; import { fetchDevices, ZHADevice } from "../../../../../data/zha"; -import { fireEvent } from "../../../../../common/dom/fire_event"; export interface DeviceRowData extends DataTableRowData { id: string; @@ -22,14 +18,21 @@ export interface DeviceRowData extends DataTableRowData { lqi: number; } -@customElement("dialog-zha-device-children") -class DialogZHADeviceChildren extends LitElement { +@customElement("zha-device-children") +class ZHADeviceChildren extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _device: ZHADevice | undefined; + @property() public device: ZHADevice | undefined; @state() private _devices: Map | undefined; + protected updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if (this.hass && changedProperties.has("device")) { + this._fetchData(); + } + } + private _deviceChildren = memoizeOne( ( device: ZHADevice | undefined, @@ -69,70 +72,45 @@ class DialogZHADeviceChildren extends LitElement { }, }; - public showDialog(params: ZHADeviceChildrenDialogParams): void { - this._device = params.device; - this._fetchData(); - } - - public closeDialog(): void { - this._device = undefined; - this._devices = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - protected render(): TemplateResult { - if (!this._device) { + if (!this.device) { return html``; } return html` - - ${!this._devices - ? html`` - : html``} - + ${!this._devices + ? html`` + : html``} `; } private async _fetchData(): Promise { - if (this._device && this.hass) { + if (this.device && this.hass) { const devices = await fetchDevices(this.hass!); this._devices = new Map( devices.map((device: ZHADevice) => [device.ieee, device]) ); } } - - static get styles(): CSSResultGroup { - return haStyleDialog; - } } declare global { interface HTMLElementTagNameMap { - "dialog-zha-device-children": DialogZHADeviceChildren; + "zha-device-children": ZHADeviceChildren; } } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts new file mode 100644 index 0000000000..0ce2b9767a --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-signature.ts @@ -0,0 +1,47 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../../../components/ha-code-editor"; +import { ZHADevice } from "../../../../../data/zha"; +import { HomeAssistant } from "../../../../../types"; + +@customElement("zha-device-zigbee-info") +class ZHADeviceZigbeeInfo extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public device: ZHADevice | undefined; + + @state() private _signature: any; + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device") && this.hass && this.device) { + this._signature = JSON.stringify( + { + ...this.device.signature, + manufacturer: this.device.manufacturer, + model: this.device.model, + class: this.device.quirk_class, + }, + null, + 2 + ); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this._signature) { + return html``; + } + + return html` + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-device-zigbee-info": ZHADeviceZigbeeInfo; + } +} diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts index 67671e9047..d96b27c9ad 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts @@ -1,5 +1,3 @@ -import "@material/mwc-button/mwc-button"; -import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, @@ -11,37 +9,29 @@ import { import { customElement, property, state, query } from "lit/decorators"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/buttons/ha-progress-button"; import { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-service-description"; import { bindDeviceToGroup, Cluster, - fetchClustersForZhaNode, + fetchClustersForZhaDevice, unbindDeviceFromGroup, ZHADevice, ZHAGroup, } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "../../../ha-config-section"; import { ItemSelectedEvent } from "./types"; import "./zha-clusters-data-table"; import type { ZHAClustersDataTable } from "./zha-clusters-data-table"; +import "@material/mwc-list/mwc-list-item"; @customElement("zha-group-binding-control") export class ZHAGroupBindingControl extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public isWide?: boolean; - - @property() public narrow?: boolean; - - @property() public selectedDevice?: ZHADevice; - - @state() private _showHelp = false; + @property() public device?: ZHADevice; @state() private _bindTargetIndex = -1; @@ -51,6 +41,8 @@ export class ZHAGroupBindingControl extends LitElement { @state() private _clusters: Cluster[] = []; + @state() private _bindingOperationInProgress = false; + private _groupToBind?: ZHAGroup; private _clustersToBind?: Cluster[]; @@ -59,38 +51,17 @@ export class ZHAGroupBindingControl extends LitElement { private _zhaClustersDataTable!: ZHAClustersDataTable; protected updated(changedProperties: PropertyValues): void { - if (changedProperties.has("selectedDevice")) { + if (changedProperties.has("device")) { this._bindTargetIndex = -1; this._selectedClusters = []; this._clustersToBind = []; this._fetchClustersForZhaNode(); } - super.update(changedProperties); + super.updated(changedProperties); } protected render(): TemplateResult { return html` - -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.header" - )} - - -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.introduction" - )} -
    - ${this._showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.group_picker_help" - )} -
    - ` - : ""}
    - ${this._showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.cluster_selection_help" - )} -
    - ` - : ""}
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_label" - )} - ${this._showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.bind_button_help" - )} -
    - ` - : ""} - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_label" - )} - ${this._showHelp - ? html` -
    - ${this.hass!.localize( - "ui.panel.config.zha.group_binding.unbind_button_help" - )} -
    - ` - : ""} + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.bind_button_label" + )} + + + + ${this.hass!.localize( + "ui.panel.config.zha.group_binding.unbind_button_label" + )} +
    @@ -186,31 +123,49 @@ export class ZHAGroupBindingControl extends LitElement { : this.groups[this._bindTargetIndex]; } - private _onHelpTap(): void { - this._showHelp = !this._showHelp; - } - - private async _onBindGroupClick(): Promise { + private async _onBindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await bindDeviceToGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await bindDeviceToGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } - private async _onUnbindGroupClick(): Promise { + private async _onUnbindGroupClick(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; if (this.hass && this._canBind) { - await unbindDeviceFromGroup( - this.hass, - this.selectedDevice!.ieee, - this._groupToBind!.group_id, - this._clustersToBind! - ); - this._zhaClustersDataTable.clearSelection(); + this._bindingOperationInProgress = true; + button.progress = true; + try { + await unbindDeviceFromGroup( + this.hass, + this.device!.ieee, + this._groupToBind!.group_id, + this._clustersToBind! + ); + this._zhaClustersDataTable.clearSelection(); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + } finally { + this._bindingOperationInProgress = false; + button.progress = false; + } } } @@ -230,9 +185,9 @@ export class ZHAGroupBindingControl extends LitElement { private async _fetchClustersForZhaNode(): Promise { if (this.hass) { - this._clusters = await fetchClustersForZhaNode( + this._clusters = await fetchClustersForZhaDevice( this.hass, - this.selectedDevice!.ieee + this.device!.ieee ); this._clusters = this._clusters .filter((cluster) => cluster.type.toLowerCase() === "out") @@ -245,7 +200,7 @@ export class ZHAGroupBindingControl extends LitElement { this._groupToBind && this._clustersToBind && this._clustersToBind?.length > 0 && - this.selectedDevice + this.device ); } @@ -257,18 +212,6 @@ export class ZHAGroupBindingControl extends LitElement { width: 100%; } - .content { - margin-top: 24px; - } - - ha-card { - max-width: 680px; - } - - .card-actions.warning ha-call-service-button { - color: var(--error-color); - } - .command-picker { align-items: center; padding-left: 28px; @@ -285,30 +228,6 @@ export class ZHAGroupBindingControl extends LitElement { .sectionHeader { flex-grow: 1; } - - .helpText { - color: grey; - padding-left: 28px; - padding-right: 28px; - padding-bottom: 10px; - } - - .toggle-help-icon { - float: right; - top: -6px; - right: 0; - padding-right: 0px; - color: var(--primary-color); - } - - ha-service-description { - display: block; - color: grey; - } - - [hidden] { - display: none; - } `, ]; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts new file mode 100644 index 0000000000..d956723242 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts @@ -0,0 +1,198 @@ +import "@material/mwc-list/mwc-list-item"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import { stopPropagation } from "../../../../../common/dom/stop_propagation"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-select"; +import { + Cluster, + fetchClustersForZhaDevice, + ZHADevice, +} from "../../../../../data/zha"; +import { haStyle } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { computeClusterKey } from "./functions"; +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import "./zha-cluster-attributes"; +import "./zha-cluster-commands"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-cluster-selected": { + cluster?: Cluster; + }; + } +} + +const tabs = ["attributes", "commands"] as const; + +@customElement("zha-manage-clusters") +export class ZHAManageClusters extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide?: boolean; + + @property() public device?: ZHADevice; + + @state() private _selectedClusterIndex = -1; + + @state() private _clusters: Cluster[] = []; + + @state() private _selectedCluster?: Cluster; + + @state() private _currTab: typeof tabs[number] = "attributes"; + + @state() private _clustersLoaded = false; + + protected willUpdate(changedProps: PropertyValues) { + super.willUpdate(changedProps); + if (!this.device) { + return; + } + if (!tabs.includes(this._currTab)) { + this._currTab = tabs[0]; + } + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("device")) { + this._clusters = []; + this._selectedClusterIndex = -1; + this._clustersLoaded = false; + this._fetchClustersForZhaDevice(); + } + super.updated(changedProperties); + } + + protected render(): TemplateResult { + if (!this.device || !this._clustersLoaded) { + return html``; + } + return html` + +
    + + ${this._clusters.map( + (entry, idx) => html` + ${computeClusterKey(entry)} + ` + )} + +
    + ${this._selectedCluster + ? html` + + ${tabs.map( + (tab) => html` + + ` + )} + + +
    + ${cache( + this._currTab === "attributes" + ? html` + + ` + : html` + + ` + )} +
    + ` + : ""} +
    + `; + } + + private async _fetchClustersForZhaDevice(): Promise { + if (this.hass) { + this._clusters = await fetchClustersForZhaDevice( + this.hass, + this.device!.ieee + ); + this._clusters.sort((a, b) => a.name.localeCompare(b.name)); + if (this._clusters.length > 0) { + this._selectedClusterIndex = 0; + this._selectedCluster = this._clusters[0]; + } + this._clustersLoaded = true; + } + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = tabs[ev.detail.index]; + if (newTab === this._currTab) { + return; + } + this._currTab = newTab; + } + + private _selectedClusterChanged(event): void { + this._selectedClusterIndex = Number(event.target!.value); + this._selectedCluster = this._clusters[this._selectedClusterIndex]; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-select { + margin-top: 16px; + } + .menu { + width: 100%; + } + .header { + flex-grow: 1; + } + .node-picker { + align-items: center; + padding-bottom: 10px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "zha-manage-clusters": ZHAManageClusters; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index a7f7dbe100..23933d2f6d 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1002,6 +1002,15 @@ "attribute": "Attribute", "min_max_change": "min/max/change" }, + "zha_manage_device": { + "heading": "Manage Zigbee Device", + "tabs": { + "clusters": "Clusters", + "bindings": "Bindings", + "signature": "Signature", + "children": "Children" + } + }, "zha_device_info": { "manuf": "by {manufacturer}", "no_area": "No Area", @@ -1010,10 +1019,8 @@ "buttons": { "add": "Add devices via this device", "remove": "Remove", - "clusters": "Manage clusters", + "manage": "Manage zigbee device", "reconfigure": "Reconfigure", - "zigbee_information": "Zigbee signature", - "device_children": "View children", "view_network": "View network" }, "services": { @@ -3078,17 +3085,17 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Select a cluster to view attributes and commands.", - "introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands." + "tabs": { + "attributes": "Attributes", + "commands": "Commands" + } }, "cluster_attributes": { "header": "Cluster Attributes", "introduction": "View and edit cluster attributes.", "attributes_of_cluster": "Attributes of the selected cluster", - "get_zigbee_attribute": "Get Zigbee Attribute", - "set_zigbee_attribute": "Set Zigbee Attribute", - "help_attribute_dropdown": "Select an attribute to view or set its value.", - "help_get_zigbee_attribute": "Get the value for the selected attribute.", - "help_set_zigbee_attribute": "Set attribute value for the specified cluster on the specified entity." + "read_zigbee_attribute": "Read Attribute", + "write_zigbee_attribute": "Write Attribute" }, "cluster_commands": { "header": "Cluster Commands", @@ -3138,6 +3145,11 @@ "enable_physics": "Enable Physics", "refresh_topology": "Refresh Topology" }, + "device_binding": { + "bind": "Bind", + "unbind": "Unbind", + "picker_label": "Bindable Devices" + }, "group_binding": { "header": "Group Binding", "introduction": "Bind and unbind groups.", From 17a11809de5af4a6b4a6652a6ce7ad3cf92b99a7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 Sep 2022 19:39:05 +0200 Subject: [PATCH 79/98] Prevent re-creation of custom panel when content was not changed (#13747) --- src/data/entity_registry.ts | 2 +- src/panels/custom/ha-panel-custom.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 075ce04b12..3802b26dca 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -61,7 +61,7 @@ export interface EntityRegistryEntryUpdateParams { hidden_by: string | null; new_entity_id?: string; options_domain?: string; - options?: SensorEntityOptions | WeatherEntityOptions; + options?: SensorEntityOptions | NumberEntityOptions | WeatherEntityOptions; } export const findBatteryEntity = ( diff --git a/src/panels/custom/ha-panel-custom.ts b/src/panels/custom/ha-panel-custom.ts index 907bc9c655..fd759575c4 100644 --- a/src/panels/custom/ha-panel-custom.ts +++ b/src/panels/custom/ha-panel-custom.ts @@ -1,6 +1,7 @@ import { PropertyValues, ReactiveElement } from "lit"; import { property } from "lit/decorators"; import { navigate, NavigateOptions } from "../../common/navigate"; +import { deepEqual } from "../../common/util/deep-equal"; import { CustomPanelInfo } from "../../data/panel_custom"; import { HomeAssistant, Route } from "../../types"; import { createCustomPanelElement } from "../../util/custom-panel/create-custom-panel-element"; @@ -54,12 +55,15 @@ export class HaPanelCustom extends ReactiveElement { protected update(changedProps: PropertyValues) { super.update(changedProps); if (changedProps.has("panel")) { - // Clean up old things if we had a panel - if (changedProps.get("panel")) { - this._cleanupPanel(); + // Clean up old things if we had a panel and the new one is different. + const oldPanel = changedProps.get("panel") as CustomPanelInfo | undefined; + if (!deepEqual(oldPanel, this.panel)) { + if (oldPanel) { + this._cleanupPanel(); + } + this._createPanel(this.panel); + return; } - this._createPanel(this.panel); - return; } if (!this._setProperties) { return; From 3c1b2aa4f3fc283817c8e774ca4845449f1020e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 08:22:16 +0200 Subject: [PATCH 80/98] Add support for overriding sensor volume unit (#13833) --- src/panels/config/entities/entity-registry-settings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 8ae1e84046..53f8f92224 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -111,8 +111,9 @@ const OVERRIDE_NUMBER_UNITS = { }; const OVERRIDE_SENSOR_UNITS = { - temperature: ["°C", "°F", "K"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], + temperature: ["°C", "°F", "K"], + volume: ["fl. oz.", "ft³", "gal", "L", "mL", "m³"], }; const OVERRIDE_WEATHER_UNITS = { From d1c2020ee40fb35bed521fad5e5eb04b10ba1dd6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 08:49:42 +0200 Subject: [PATCH 81/98] Add support for overriding sensor speed unit (#13832) --- src/panels/config/entities/entity-registry-settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index 53f8f92224..d19a219cb3 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -112,6 +112,7 @@ const OVERRIDE_NUMBER_UNITS = { const OVERRIDE_SENSOR_UNITS = { pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], + speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mph"], temperature: ["°C", "°F", "K"], volume: ["fl. oz.", "ft³", "gal", "L", "mL", "m³"], }; From 296d5f8ffe80721c409e45920de8c3d60ac29a1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 09:11:34 +0200 Subject: [PATCH 82/98] Add support for overriding sensor distance unit (#13831) --- src/panels/config/entities/entity-registry-settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index d19a219cb3..cba8909dde 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -111,6 +111,7 @@ const OVERRIDE_NUMBER_UNITS = { }; const OVERRIDE_SENSOR_UNITS = { + distance: ["cm", "ft", "in", "km", "m", "mi", "mm", "yd"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mph"], temperature: ["°C", "°F", "K"], From 894258d7b8d235046dfcea173fb3541f1e52269d Mon Sep 17 00:00:00 2001 From: Meow Date: Wed, 28 Sep 2022 10:53:25 +0200 Subject: [PATCH 83/98] fixed duplicate unit_of_measurement (#13884) --- .../src/pages/more-info/input-number.markdown | 3 + gallery/src/pages/more-info/input-number.ts | 60 +++++++++++++++++++ src/state-summary/state-card-input_number.js | 3 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 gallery/src/pages/more-info/input-number.markdown create mode 100644 gallery/src/pages/more-info/input-number.ts diff --git a/gallery/src/pages/more-info/input-number.markdown b/gallery/src/pages/more-info/input-number.markdown new file mode 100644 index 0000000000..20babab09b --- /dev/null +++ b/gallery/src/pages/more-info/input-number.markdown @@ -0,0 +1,3 @@ +--- +title: Input Number +--- diff --git a/gallery/src/pages/more-info/input-number.ts b/gallery/src/pages/more-info/input-number.ts new file mode 100644 index 0000000000..e070b52e8b --- /dev/null +++ b/gallery/src/pages/more-info/input-number.ts @@ -0,0 +1,60 @@ +import { html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import "../../../../src/components/ha-card"; +import "../../../../src/dialogs/more-info/more-info-content"; +import { getEntity } from "../../../../src/fake_data/entity"; +import { + MockHomeAssistant, + provideHass, +} from "../../../../src/fake_data/provide_hass"; +import "../../components/demo-more-infos"; + +const ENTITIES = [ + getEntity("input_number", "box1", 0, { + friendly_name: "Box1", + min: 0, + max: 100, + step: 1, + initial: 0, + mode: "box", + unit_of_measurement: "items", + }), + getEntity("input_number", "slider1", 0, { + friendly_name: "Slider1", + min: 0, + max: 100, + step: 1, + initial: 0, + mode: "slider", + unit_of_measurement: "items", + }), +]; + +@customElement("demo-more-info-input-number") +class DemoMoreInfoInputNumber extends LitElement { + @property() public hass!: MockHomeAssistant; + + @query("demo-more-infos") private _demoRoot!: HTMLElement; + + protected render(): TemplateResult { + return html` + ent.entityId)} + > + `; + } + + protected firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + const hass = provideHass(this._demoRoot); + hass.updateTranslations(null, "en"); + hass.addEntities(ENTITIES); + } +} + +declare global { + interface HTMLElementTagNameMap { + "demo-more-info-input-number": DemoMoreInfoInputNumber; + } +} diff --git a/src/state-summary/state-card-input_number.js b/src/state-summary/state-card-input_number.js index 2e0cd3f8b7..49efced50a 100644 --- a/src/state-summary/state-card-input_number.js +++ b/src/state-summary/state-card-input_number.js @@ -31,8 +31,7 @@ class StateCardInputNumber extends mixinBehaviors( .sliderstate { min-width: 45px; } - ha-slider[hidden], - ha-textfield[hidden] { + [hidden] { display: none !important; } ha-textfield { From be30cdb51f673889c23f1be9787d3c0bee1100b5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 11:00:36 +0200 Subject: [PATCH 84/98] Update change password dialog (#13888) * Update change password dialog * Improve wording * Update modal for people page * Remove useless async --- .../config/person/dialog-person-detail.ts | 40 +--- .../users/dialog-admin-change-password.ts | 183 ++++++++++++++++++ src/panels/config/users/dialog-user-detail.ts | 49 +---- .../show-dialog-admin-change-password.ts | 19 ++ src/translations/en.json | 10 +- 5 files changed, 220 insertions(+), 81 deletions(-) create mode 100644 src/panels/config/users/dialog-admin-change-password.ts create mode 100644 src/panels/config/users/show-dialog-admin-change-password.ts diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index c9d0c0c56a..a847150f07 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -5,10 +5,9 @@ import memoizeOne from "memoize-one"; import "../../../components/entity/ha-entities-picker"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; -import "../../../components/ha-textfield"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; -import { adminChangePassword } from "../../../data/auth"; +import "../../../components/ha-textfield"; import { PersonMutableParams } from "../../../data/person"; import { deleteUser, @@ -20,7 +19,6 @@ import { import { showAlertDialog, showConfirmationDialog, - showPromptDialog, } from "../../../dialogs/generic/show-dialog-box"; import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { PolymerChangedEvent } from "../../../polymer-types"; @@ -28,6 +26,7 @@ import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showAddUserDialog } from "../users/show-dialog-add-user"; +import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; const includeDomains = ["device_tracker"]; @@ -350,40 +349,7 @@ class DialogPersonDetail extends LitElement { }); return; } - const newPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.editor.new_password" - ), - }); - if (!newPassword) { - return; - } - const confirmPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.add_user.password_confirm" - ), - }); - if (!confirmPassword) { - return; - } - if (newPassword !== confirmPassword) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.add_user.password_not_match" - ), - }); - return; - } - await adminChangePassword(this.hass, this._user.id, newPassword); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.editor.password_changed" - ), - }); + showAdminChangePasswordDialog(this, { userId: this._user.id }); } private async _updateEntry() { diff --git a/src/panels/config/users/dialog-admin-change-password.ts b/src/panels/config/users/dialog-admin-change-password.ts new file mode 100644 index 0000000000..227d4fb057 --- /dev/null +++ b/src/panels/config/users/dialog-admin-change-password.ts @@ -0,0 +1,183 @@ +import "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; + +import { fireEvent } from "../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import { SchemaUnion } from "../../../components/ha-form/types"; +import "../../../components/ha-textfield"; +import { adminChangePassword } from "../../../data/auth"; +import { haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { showToast } from "../../../util/toast"; +import { AdminChangePasswordDialogParams } from "./show-dialog-admin-change-password"; + +const SCHEMA = [ + { + name: "new_password", + required: true, + selector: { + text: { + type: "password", + }, + }, + }, + { + name: "password_confirm", + required: true, + selector: { + text: { + type: "password", + }, + }, + }, +] as const; + +type FormData = { new_password?: string; password_confirm?: string }; + +@customElement("dialog-admin-change-password") +class DialogAdminChangePassword extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: AdminChangePasswordDialogParams; + + @state() private _userId?: string; + + @state() private _data?: FormData; + + @state() private _error?: Record; + + @state() private _submitting = false; + + @state() private _success = false; + + public showDialog(params: AdminChangePasswordDialogParams): void { + this._params = params; + this._userId = params.userId; + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + this._submitting = false; + this._success = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _computeLabel = (schema: SchemaUnion) => + this.hass.localize(`ui.panel.config.users.change_password.${schema.name}`); + + private _computeError = (error: string) => + this.hass.localize( + `ui.panel.config.users.change_password.${error}` as any + ) || error; + + private _validate() { + if ( + this._data && + this._data.new_password && + this._data.password_confirm && + this._data.new_password !== this._data.password_confirm + ) { + this._error = { + password_confirm: "password_no_match", + }; + } else { + this._error = undefined; + } + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + const canSubmit = Boolean( + this._data?.new_password && this._data?.password_confirm && !this._error + ); + + return html` + + ${this._success + ? html` +

    + ${this.hass.localize( + "ui.panel.config.users.change_password.password_changed" + )} +

    + + ${this.hass.localize("ui.dialogs.generic.ok")} + + ` + : html` + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.panel.config.users.change_password.change" + )} + + `} +
    + `; + } + + private _valueChanged(ev) { + this._data = ev.detail.value; + this._validate(); + } + + private async _changePassword(): Promise { + if (!this._userId || !this._data?.new_password) return; + try { + this._submitting = true; + await adminChangePassword( + this.hass, + this._userId!, + this._data.new_password + ); + this._success = true; + } catch (err: any) { + showToast(this, { + message: err.body?.message || err.message || err, + }); + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-admin-change-password": DialogAdminChangePassword; + } +} diff --git a/src/panels/config/users/dialog-user-detail.ts b/src/panels/config/users/dialog-user-detail.ts index 490839b3cd..da3ced4d61 100644 --- a/src/panels/config/users/dialog-user-detail.ts +++ b/src/panels/config/users/dialog-user-detail.ts @@ -4,26 +4,23 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-chip"; +import "../../../components/ha-chip-set"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; import "../../../components/ha-help-tooltip"; -import "../../../components/ha-chip-set"; -import "../../../components/ha-chip"; import "../../../components/ha-svg-icon"; -import "../../../components/ha-textfield"; import "../../../components/ha-switch"; -import { adminChangePassword } from "../../../data/auth"; +import "../../../components/ha-textfield"; import { computeUserBadges, SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_USER, } from "../../../data/user"; -import { - showAlertDialog, - showPromptDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password"; import { UserDetailDialogParams } from "./show-dialog-user-detail"; @customElement("dialog-user-detail") @@ -268,40 +265,8 @@ class DialogUserDetail extends LitElement { }); return; } - const newPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.editor.new_password" - ), - }); - if (!newPassword) { - return; - } - const confirmPassword = await showPromptDialog(this, { - title: this.hass.localize("ui.panel.config.users.editor.change_password"), - inputType: "password", - inputLabel: this.hass.localize( - "ui.panel.config.users.add_user.password_confirm" - ), - }); - if (!confirmPassword) { - return; - } - if (newPassword !== confirmPassword) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.add_user.password_not_match" - ), - }); - return; - } - await adminChangePassword(this.hass, this._params!.entry.id, newPassword); - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.users.editor.password_changed" - ), - }); + + showAdminChangePasswordDialog(this, { userId: this._params!.entry.id }); } private _close(): void { diff --git a/src/panels/config/users/show-dialog-admin-change-password.ts b/src/panels/config/users/show-dialog-admin-change-password.ts new file mode 100644 index 0000000000..df6772a5c1 --- /dev/null +++ b/src/panels/config/users/show-dialog-admin-change-password.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export interface AdminChangePasswordDialogParams { + userId: string; +} + +export const loadAdminChangePasswordDialog = () => + import("./dialog-admin-change-password"); + +export const showAdminChangePasswordDialog = ( + element: HTMLElement, + dialogParams: AdminChangePasswordDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-admin-change-password", + dialogImport: loadAdminChangePasswordDialog, + dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 23933d2f6d..adc27b77f7 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2988,8 +2988,6 @@ "name": "Display name", "username": "Username", "change_password": "Change password", - "new_password": "New Password", - "password_changed": "Password was changed successfully", "activate_user": "Activate user", "deactivate_user": "Deactivate user", "delete_user": "Delete user", @@ -3015,6 +3013,14 @@ "password_not_match": "Passwords don't match", "local_only": "Local only", "create": "Create" + }, + "change_password": { + "caption": "Change password", + "new_password": "New Password", + "password_confirm": "Confirm Password", + "change": "Change", + "password_no_match": "Passwords don't match", + "password_changed": "The password has been changed successfully." } }, "application_credentials": { From 4a07d3d39b0806263d52af6450f7ee31303b1c0d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 11:01:12 +0200 Subject: [PATCH 85/98] Update Restart add-on dialog (#13896) --- hassio/src/dialogs/suggestAddonRestart.ts | 14 +++++++------- src/translations/en.json | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/hassio/src/dialogs/suggestAddonRestart.ts b/hassio/src/dialogs/suggestAddonRestart.ts index 66e95bf08b..56a7bb8a0e 100644 --- a/hassio/src/dialogs/suggestAddonRestart.ts +++ b/hassio/src/dialogs/suggestAddonRestart.ts @@ -18,9 +18,11 @@ export const suggestAddonRestart = async ( addon: HassioAddonDetails ): Promise => { const confirmed = await showConfirmationDialog(element, { - title: supervisor.localize("common.restart_name", "name", addon.name), + title: supervisor.localize("dialog.restart_addon.title", { + name: addon.name, + }), text: supervisor.localize("dialog.restart_addon.text"), - confirmText: supervisor.localize("dialog.restart_addon.confirm_text"), + confirmText: supervisor.localize("dialog.restart_addon.restart"), dismissText: supervisor.localize("common.cancel"), }); if (confirmed) { @@ -28,11 +30,9 @@ export const suggestAddonRestart = async ( await restartHassioAddon(hass, addon.slug); } catch (err: any) { showAlertDialog(element, { - title: supervisor.localize( - "common.failed_to_restart_name", - "name", - addon.name - ), + title: supervisor.localize("common.failed_to_restart_name", { + name: addon.name, + }), text: extractApiErrorMessage(err), }); } diff --git a/src/translations/en.json b/src/translations/en.json index adc27b77f7..3751ef5866 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5129,8 +5129,9 @@ "used": "Repository is in use for installed add-ons and can't be removed." }, "restart_addon": { - "confirm_text": "Restart add-on", - "text": "Do you want to restart the add-on with your changes?" + "title": "Restart {name}?", + "text": "To use the new saved configuration this add-on must be restarted.", + "restart": "Restart" }, "hardware": { "title": "Hardware", From 61d09072a7ca6da7470652a01cda90f87dd9a7e5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 11:02:09 +0200 Subject: [PATCH 86/98] Update Uninstall add-on dialog (#13898) --- hassio/src/addon-view/info/hassio-addon-info.ts | 11 +++++++---- src/translations/en.json | 5 +++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index 3753b8a42d..3cc3d23010 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -1024,10 +1024,13 @@ class HassioAddonInfo extends LitElement { button.progress = true; const confirmed = await showConfirmationDialog(this, { - title: this.addon.name, - text: "Are you sure you want to uninstall this add-on?", - confirmText: "uninstall add-on", - dismissText: "no", + title: this.supervisor.localize("dialog.uninstall_addon.title", { + name: this.addon.name, + }), + text: this.supervisor.localize("dialog.uninstall_addon.text"), + confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"), + dismissText: this.supervisor.localize("common.cancel"), + destructive: true, }); if (!confirmed) { diff --git a/src/translations/en.json b/src/translations/en.json index 3751ef5866..21f081d1a4 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5133,6 +5133,11 @@ "text": "To use the new saved configuration this add-on must be restarted.", "restart": "Restart" }, + "uninstall_addon": { + "title": "Uninstall {name}?", + "text": "Its configuration will be permanently deleted.", + "uninstall": "Uninstall" + }, "hardware": { "title": "Hardware", "search": "Search hardware", From 3959a7475c835d67fa7d52e7d46d909f31e6257c Mon Sep 17 00:00:00 2001 From: Ignacio Hernandez-Ros Date: Wed, 28 Sep 2022 11:08:30 +0200 Subject: [PATCH 87/98] Bug Fix, can't create schedule on Sunday (#13876) --- src/panels/config/helpers/forms/ha-schedule-form.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/panels/config/helpers/forms/ha-schedule-form.ts b/src/panels/config/helpers/forms/ha-schedule-form.ts index a3387f9483..c28e90f8f0 100644 --- a/src/panels/config/helpers/forms/ha-schedule-form.ts +++ b/src/panels/config/helpers/forms/ha-schedule-form.ts @@ -206,6 +206,7 @@ class HaScheduleForm extends LitElement { private get _events() { const events: any[] = []; const currentDay = new Date().getDay(); + const baseDay = currentDay === 0 ? 7 : currentDay; for (const [i, day] of weekdays.entries()) { if (!this[`_${day}`].length) { @@ -214,7 +215,7 @@ class HaScheduleForm extends LitElement { this[`_${day}`].forEach((item: ScheduleDay, index: number) => { // Add 7 to 0 because we start the calendar on Monday - const distance = i - currentDay + (i === 0 ? 7 : 0); + const distance = i - baseDay + (i === 0 ? 7 : 0); const start = new Date(); start.setDate(start.getDate() + distance); From fc86a66c33b2768f13aa73d4369f1b6c60d1030f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 11:27:09 +0200 Subject: [PATCH 88/98] Use unique id for script (#13817) Co-authored-by: Bram Kragten --- src/data/script.ts | 5 +- src/panels/config/script/ha-config-script.ts | 4 +- src/panels/config/script/ha-script-editor.ts | 105 ++++++++++--------- src/panels/config/script/ha-script-picker.ts | 23 ++-- src/panels/config/script/ha-script-trace.ts | 50 +++++---- src/panels/logbook/ha-logbook-renderer.ts | 6 +- 6 files changed, 100 insertions(+), 93 deletions(-) diff --git a/src/data/script.ts b/src/data/script.ts index 1f925cffe3..ec13344714 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -15,7 +15,6 @@ import { Describe, boolean, } from "superstruct"; -import { computeObjectId } from "../common/entity/compute_object_id"; import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; import { @@ -278,9 +277,9 @@ export type ActionType = keyof ActionTypes; export const triggerScript = ( hass: HomeAssistant, - entityId: string, + scriptId: string, variables?: Record -) => hass.callService("script", computeObjectId(entityId), variables); +) => hass.callService("script", scriptId, variables); export const canRun = (state: ScriptEntity) => { if (state.state === "off") { diff --git a/src/panels/config/script/ha-config-script.ts b/src/panels/config/script/ha-config-script.ts index 4b8f9aefa8..6aabf90bf4 100644 --- a/src/panels/config/script/ha-config-script.ts +++ b/src/panels/config/script/ha-config-script.ts @@ -88,8 +88,8 @@ class HaConfigScript extends HassRouterPage { this._currentPage !== "dashboard" ) { pageEl.creatingNew = undefined; - const scriptEntityId = this.routeTail.path.substr(1); - pageEl.scriptEntityId = scriptEntityId === "new" ? null : scriptEntityId; + const scriptId = this.routeTail.path.substr(1); + pageEl.scriptId = scriptId === "new" ? null : scriptId; } } } diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 629cff5412..34eb30b46d 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -24,7 +24,6 @@ import { property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import { computeObjectId } from "../../../common/entity/compute_object_id"; import { navigate } from "../../../common/navigate"; import { slugify } from "../../../common/string/slugify"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -68,7 +67,7 @@ import type { HaManualScriptEditor } from "./manual-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public scriptEntityId: string | null = null; + @property() public scriptId: string | null = null; @property({ attribute: false }) public route!: Route; @@ -162,7 +161,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } const schema = this._schema( - !!this.scriptEntityId, + !!this.scriptId, "use_blueprint" in this._config, this._config.mode ); @@ -183,7 +182,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { .backCallback=${this._backTapped} .header=${!this._config?.alias ? "" : this._config.alias} > - ${this.scriptEntityId && !this.narrow + ${this.scriptId && !this.narrow ? html` ${this.hass.localize( @@ -201,7 +200,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${this.hass.localize("ui.panel.config.script.editor.show_info")} @@ -213,16 +212,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${this.hass.localize("ui.panel.config.script.picker.run_script")} - ${this.scriptEntityId && this.narrow + ${this.scriptId && this.narrow ? html` - + ${this.hass.localize( "ui.panel.config.script.editor.show_trace" @@ -295,7 +294,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
  • ${this.hass.localize("ui.panel.config.script.picker.delete")} @@ -430,15 +429,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { protected updated(changedProps: PropertyValues): void { super.updated(changedProps); - const oldScript = changedProps.get("scriptEntityId"); + const oldScript = changedProps.get("scriptId"); if ( - changedProps.has("scriptEntityId") && - this.scriptEntityId && + changedProps.has("scriptId") && + this.scriptId && this.hass && // Only refresh config if we picked a new script. If same ID, don't fetch it. - (!oldScript || oldScript !== this.scriptEntityId) + (!oldScript || oldScript !== this.scriptId) ) { - getScriptConfig(this.hass, computeObjectId(this.scriptEntityId)).then( + getScriptConfig(this.hass, this.scriptId).then( (config) => { // Normalize data: ensure sequence is a list // Happens when people copy paste their scripts into the config @@ -458,7 +457,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { : this.hass.localize( "ui.panel.config.script.editor.load_error_unknown", "err_no", - resp.status_code + resp.status_code || resp.code ) ); history.back(); @@ -466,11 +465,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ); } - if ( - changedProps.has("scriptEntityId") && - !this.scriptEntityId && - this.hass - ) { + if (changedProps.has("scriptId") && !this.scriptId && this.hass) { const initData = getScriptEditorInitData(); this._dirty = !!initData; const baseConfig: Partial = { @@ -530,24 +525,30 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }; private async _showInfo() { - if (!this.scriptEntityId) { + if (!this.scriptId) { return; } - fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); + const entity = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + ); + if (!entity) { + return; + } + fireEvent(this, "hass-more-info", { entityId: entity.entity_id }); } private async _showTrace() { - if (this.scriptEntityId) { + if (this.scriptId) { const result = await this.confirmUnsavedChanged(); if (result) { - navigate(`/config/script/trace/${this.scriptEntityId}`); + navigate(`/config/script/trace/${this.scriptId}`); } } } private async _runScript(ev: CustomEvent) { ev.stopPropagation(); - await triggerScript(this.hass, this.scriptEntityId as string); + await triggerScript(this.hass, this.scriptId!); showToast(this, { message: this.hass.localize( "ui.notification_toast.triggered", @@ -613,7 +614,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { max: isMaxMode(values.mode) ? values.max : undefined, }; - if (!this.scriptEntityId) { + if (!this.scriptId) { this.updateEntityId(values.id, values.alias); } @@ -720,10 +721,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } private async _delete() { - await deleteScript( - this.hass, - computeObjectId(this.scriptEntityId as string) - ); + await deleteScript(this.hass, this.scriptId!); history.back(); } @@ -741,7 +739,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } - private _saveScript(): void { + private async _saveScript(): Promise { if (this._idError) { showToast(this, { message: this.hass.localize( @@ -756,24 +754,27 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }); return; } - const id = this.scriptEntityId - ? computeObjectId(this.scriptEntityId) - : this._entityId || Date.now(); - this.hass!.callApi("POST", "config/script/config/" + id, this._config).then( - () => { - this._dirty = false; - if (!this.scriptEntityId) { - navigate(`/config/script/edit/${id}`, { replace: true }); - } - }, - (errors) => { - this._errors = errors.body.message || errors.error || errors.body; - showToast(this, { - message: errors.body.message || errors.error || errors.body, - }); - throw errors; - } - ); + + const id = this.scriptId || this._entityId || Date.now(); + try { + await this.hass!.callApi( + "POST", + "config/script/config/" + id, + this._config + ); + } catch (errors: any) { + this._errors = errors.body.message || errors.error || errors.body; + showToast(this, { + message: errors.body.message || errors.error || errors.body, + }); + throw errors; + } + + this._dirty = false; + + if (!this.scriptId) { + navigate(`/config/script/edit/${id}`, { replace: true }); + } } protected handleKeyboardSave() { diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 8aaa8a02f4..ef55423c10 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -13,7 +13,6 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; -import { computeObjectId } from "../../../common/entity/compute_object_id"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; @@ -252,11 +251,15 @@ class HaScriptPicker extends LitElement { } private _handleRowClicked(ev: HASSDomEvent) { - navigate(`/config/script/edit/${ev.detail.id}`); + const entry = this.hass.entities[ev.detail.id]; + if (entry) { + navigate(`/config/script/edit/${entry.unique_id}`); + } } private _runScript = async (script: any) => { - await triggerScript(this.hass, script.entity_id); + const entry = this.hass.entities[script.entity_id]; + await triggerScript(this.hass, entry.unique_id); showToast(this, { message: this.hass.localize( "ui.notification_toast.triggered", @@ -271,7 +274,10 @@ class HaScriptPicker extends LitElement { } private _showTrace(script: any) { - navigate(`/config/script/trace/${script.entity_id}`); + const entry = this.hass.entities[script.entity_id]; + if (entry) { + navigate(`/config/script/trace/${entry.unique_id}`); + } } private _showHelp() { @@ -294,10 +300,8 @@ class HaScriptPicker extends LitElement { private async _duplicate(script: any) { try { - const config = await getScriptConfig( - this.hass, - computeObjectId(script.entity_id) - ); + const entry = this.hass.entities[script.entity_id]; + const config = await getScriptConfig(this.hass, entry.unique_id); showScriptEditor({ ...config, alias: `${config?.alias} (${this.hass.localize( @@ -338,7 +342,8 @@ class HaScriptPicker extends LitElement { private async _delete(script: any) { try { - await deleteScript(this.hass, computeObjectId(script.entity_id)); + const entry = this.hass.entities[script.entity_id]; + await deleteScript(this.hass, entry.unique_id); } catch (err: any) { await showAlertDialog(this, { text: diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index ab35977cb1..ae001665ca 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -44,7 +44,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; export class HaScriptTrace extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public scriptEntityId!: string; + @property() public scriptId!: string; @property({ attribute: false }) public scripts!: ScriptEntity[]; @@ -54,6 +54,8 @@ export class HaScriptTrace extends LitElement { @property({ attribute: false }) public route!: Route; + @state() private _entityId?: string; + @state() private _traces?: ScriptTrace[]; @state() private _runId?: string; @@ -74,15 +76,15 @@ export class HaScriptTrace extends LitElement { @query("hat-script-graph") private _graph?: HatScriptGraph; protected render(): TemplateResult { - const stateObj = this.scriptEntityId - ? this.hass.states[this.scriptEntityId] + const stateObj = this._entityId + ? this.hass.states[this._entityId] : undefined; const graph = this._graph; const trackedNodes = graph?.trackedNodes; const renderedNodes = graph?.renderedNodes; - const title = stateObj?.attributes.friendly_name || this.scriptEntityId; + const title = stateObj?.attributes.friendly_name || this._entityId; let devButtons: TemplateResult | string = ""; if (__DEV__) { @@ -95,11 +97,11 @@ export class HaScriptTrace extends LitElement { return html` ${devButtons} - ${!this.narrow && this.scriptEntityId + ${!this.narrow && this.scriptId ? html`
    @@ -120,7 +122,7 @@ export class HaScriptTrace extends LitElement { ${this.hass.localize("ui.panel.config.script.editor.show_info")} @@ -130,11 +132,11 @@ export class HaScriptTrace extends LitElement { > - ${this.narrow && this.scriptEntityId + ${this.narrow && this.scriptId ? html` ${this.hass.localize( @@ -309,25 +311,33 @@ export class HaScriptTrace extends LitElement { protected firstUpdated(changedProps) { super.firstUpdated(changedProps); - if (!this.scriptEntityId) { + if (!this.scriptId) { return; } const params = new URLSearchParams(location.search); this._loadTraces(params.get("run_id") || undefined); + + this._entityId = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + )?.entity_id; } public willUpdate(changedProps) { super.willUpdate(changedProps); - // Only reset if scriptEntityId has changed and we had one before. - if (changedProps.get("scriptEntityId")) { + // Only reset if scriptId has changed and we had one before. + if (changedProps.get("scriptId")) { this._traces = undefined; this._runId = undefined; this._trace = undefined; this._logbookEntries = undefined; - if (this.scriptEntityId) { + if (this.scriptId) { this._loadTraces(); + + this._entityId = Object.values(this.hass.entities).find( + (entry) => entry.unique_id === this.scriptId + )?.entity_id; } } @@ -364,11 +374,7 @@ export class HaScriptTrace extends LitElement { } private async _loadTraces(runId?: string) { - this._traces = await loadTraces( - this.hass, - "script", - this.scriptEntityId.split(".")[1] - ); + this._traces = await loadTraces(this.hass, "script", this.scriptId); // Newest will be on top. this._traces.reverse(); @@ -410,7 +416,7 @@ export class HaScriptTrace extends LitElement { const trace = await loadTrace( this.hass, "script", - this.scriptEntityId.split(".")[1], + this.scriptId, this._runId! ); this._logbookEntries = isComponentLoaded(this.hass, "logbook") @@ -426,7 +432,7 @@ export class HaScriptTrace extends LitElement { private _downloadTrace() { const aEl = document.createElement("a"); - aEl.download = `trace ${this.scriptEntityId} ${ + aEl.download = `trace ${this._entityId} ${ this._trace!.timestamp.start }.json`; aEl.href = `data:application/json;charset=utf-8,${encodeURI( @@ -476,10 +482,10 @@ export class HaScriptTrace extends LitElement { } private async _showInfo() { - if (!this.scriptEntityId) { + if (!this._entityId) { return; } - fireEvent(this, "hass-more-info", { entityId: this.scriptEntityId }); + fireEvent(this, "hass-more-info", { entityId: this._entityId }); } static get styles(): CSSResultGroup { diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index c79bdc9e34..74321e70bc 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -170,11 +170,7 @@ class HaLogbookRenderer extends LitElement {
    From 5146fa1d9e58bfe567ea9785a545b77218d5fcf8 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 11:27:34 +0200 Subject: [PATCH 89/98] Update join beta dialog (#13847) --- hassio/src/system/hassio-supervisor-info.ts | 44 +++---- .../config/core/ha-config-section-updates.ts | 50 +++----- .../config/core/updates/dialog-join-beta.ts | 108 ++++++++++++++++++ .../core/updates/show-dialog-join-beta.ts | 18 +++ src/translations/en.json | 13 +-- 5 files changed, 167 insertions(+), 66 deletions(-) create mode 100644 src/panels/config/core/updates/dialog-join-beta.ts create mode 100644 src/panels/config/core/updates/show-dialog-join-beta.ts diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index e1acfc74f1..1dbca65417 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -23,6 +23,7 @@ import { showAlertDialog, showConfirmationDialog, } from "../../../src/dialogs/generic/show-dialog-box"; +import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta"; import { UNHEALTHY_REASON_URL, UNSUPPORTED_REASON_URL, @@ -230,36 +231,27 @@ class HassioSupervisorInfo extends LitElement { button.progress = true; if (this.supervisor.supervisor.channel === "stable") { - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("system.supervisor.warning"), - text: html`${this.supervisor.localize("system.supervisor.beta_warning")} -
    - ${this.supervisor.localize("system.supervisor.beta_backup")} -

    - ${this.supervisor.localize("system.supervisor.beta_release_items")} -
      -
    • Home Assistant Core
    • -
    • Home Assistant Supervisor
    • -
    • Home Assistant Operating System
    • -
    -
    - ${this.supervisor.localize("system.supervisor.beta_join_confirm")}`, - confirmText: this.supervisor.localize( - "system.supervisor.join_beta_action" - ), - dismissText: this.supervisor.localize("common.cancel"), + showJoinBetaDialog(this, { + join: async () => { + await this._setChannel("beta"); + button.progress = false; + }, + cancel: () => { + button.progress = false; + }, }); - - if (!confirmed) { - button.progress = false; - return; - } + } else { + await this._setChannel("stable"); + button.progress = false; } + } + private async _setChannel( + channel: SupervisorOptions["channel"] + ): Promise { try { const data: Partial = { - channel: - this.supervisor.supervisor.channel === "stable" ? "beta" : "stable", + channel, }; await setSupervisorOption(this.hass, data); await this._reloadSupervisor(); @@ -270,8 +262,6 @@ class HassioSupervisorInfo extends LitElement { ), text: extractApiErrorMessage(err), }); - } finally { - button.progress = false; } } diff --git a/src/panels/config/core/ha-config-section-updates.ts b/src/panels/config/core/ha-config-section-updates.ts index 4b2da37924..cbf8804759 100644 --- a/src/panels/config/core/ha-config-section-updates.ts +++ b/src/panels/config/core/ha-config-section-updates.ts @@ -24,13 +24,11 @@ import { checkForEntityUpdates, filterUpdateEntitiesWithInstall, } from "../../../data/update"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import "../dashboard/ha-config-updates"; +import { showJoinBetaDialog } from "./updates/show-dialog-join-beta"; @customElement("ha-config-section-updates") class HaConfigSectionUpdates extends LitElement { @@ -46,9 +44,7 @@ class HaConfigSectionUpdates extends LitElement { super.firstUpdated(changedProps); if (isComponentLoaded(this.hass, "hassio")) { - fetchHassioSupervisorInfo(this.hass).then((data) => { - this._supervisorInfo = data; - }); + this._refreshSupervisorInfo(); } } @@ -126,6 +122,10 @@ class HaConfigSectionUpdates extends LitElement { `; } + private async _refreshSupervisorInfo() { + this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); + } + private _toggleSkipped(ev: CustomEvent): void { if (ev.detail.source !== "property") { return; @@ -142,35 +142,23 @@ class HaConfigSectionUpdates extends LitElement { } if (this._supervisorInfo!.channel === "stable") { - const confirmed = await showConfirmationDialog(this, { - title: this.hass.localize("ui.dialogs.join_beta_channel.title"), - text: html`${this.hass.localize("ui.dialogs.join_beta_channel.warning")} -
    - ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} -

    - ${this.hass.localize("ui.dialogs.join_beta_channel.release_items")} -
      -
    • Home Assistant Core
    • -
    • Home Assistant Supervisor
    • -
    • Home Assistant Operating System
    • -
    -
    - ${this.hass.localize("ui.dialogs.join_beta_channel.confirm")}`, - confirmText: this.hass.localize("ui.panel.config.updates.join_beta"), - dismissText: this.hass.localize("ui.common.cancel"), + showJoinBetaDialog(this, { + join: async () => this._setChannel("beta"), }); - - if (!confirmed) { - return; - } + } else { + this._setChannel("stable"); } + } + private async _setChannel( + channel: SupervisorOptions["channel"] + ): Promise { try { - const data: Partial = { - channel: this._supervisorInfo!.channel === "stable" ? "beta" : "stable", - }; - await setSupervisorOption(this.hass, data); + await setSupervisorOption(this.hass, { + channel, + }); await reloadSupervisor(this.hass); + await this._refreshSupervisorInfo(); } catch (err: any) { showAlertDialog(this, { text: extractApiErrorMessage(err), diff --git a/src/panels/config/core/updates/dialog-join-beta.ts b/src/panels/config/core/updates/dialog-join-beta.ts new file mode 100644 index 0000000000..41723c88ad --- /dev/null +++ b/src/panels/config/core/updates/dialog-join-beta.ts @@ -0,0 +1,108 @@ +import "@material/mwc-button/mwc-button"; +import { mdiOpenInNew } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; +import { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { JoinBetaDialogParams } from "./show-dialog-join-beta"; + +@customElement("dialog-join-beta") +export class DialogJoinBeta + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _dialogParams?: JoinBetaDialogParams; + + public showDialog(dialogParams: JoinBetaDialogParams): void { + this._dialogParams = dialogParams; + } + + public closeDialog(): void { + this._dialogParams = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._dialogParams) { + return html``; + } + + return html` + + + ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} + +

    + ${this.hass.localize("ui.dialogs.join_beta_channel.warning")} + ${this.hass.localize("ui.dialogs.join_beta_channel.release_items")} +

    +
      +
    • Home Assistant Core
    • +
    • Home Assistant Supervisor
    • +
    • Home Assistant Operating System
    • +
    +
    + ${this.hass!.localize( + "ui.dialogs.join_beta_channel.view_documentation" + )} + + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.dialogs.join_beta_channel.join")} + + + `; + } + + private _cancel() { + this._dialogParams?.cancel?.(); + this.closeDialog(); + } + + private _join() { + this._dialogParams?.join?.(); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + a { + text-decoration: none; + } + a ha-svg-icon { + --mdc-icon-size: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-join-beta": DialogJoinBeta; + } +} diff --git a/src/panels/config/core/updates/show-dialog-join-beta.ts b/src/panels/config/core/updates/show-dialog-join-beta.ts new file mode 100644 index 0000000000..515f4d517c --- /dev/null +++ b/src/panels/config/core/updates/show-dialog-join-beta.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import "./dialog-join-beta"; + +export interface JoinBetaDialogParams { + join?: () => any; + cancel?: () => any; +} + +export const showJoinBetaDialog = ( + element: HTMLElement, + dialogParams: JoinBetaDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-join-beta", + dialogImport: () => import("./dialog-join-beta"), + dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 21f081d1a4..9bca98f512 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1090,10 +1090,11 @@ }, "join_beta_channel": { "title": "Join the beta channel", - "warning": "[%key:supervisor::system::supervisor::beta_warning%]", - "backup": "[%key:supervisor::system::supervisor::beta_backup%]", - "release_items": "[%key:supervisor::system::supervisor::beta_release_items%]", - "confirm": "[%key:supervisor::system::supervisor::beta_join_confirm%]" + "backup": "Make sure you have backups of your data before you activate this feature.", + "warning": "Beta releases are for testers and early adopters and can contain unstable code changes", + "release_items": "This includes beta releases for:", + "view_documentation": "View documentation", + "join": "Join" } }, "duration": { @@ -4999,10 +5000,6 @@ "reload_supervisor": "Reload Supervisor", "warning": "WARNING", "search": "Search", - "beta_warning": "Beta releases are for testers and early adopters and can contain unstable code changes", - "beta_backup": "Make sure you have backups of your data before you activate this feature.", - "beta_release_items": "This includes beta releases for:", - "beta_join_confirm": "Do you want to join the beta channel?", "share_diagonstics_title": "Help Improve Home Assistant", "share_diagonstics_description": "Would you want to automatically share crash reports and diagnostic information when the Supervisor encounters unexpected errors? {line_break} This will allow us to fix the problems, the information is only accessible to the Home Assistant Core team and will not be shared with others.{line_break} The data does not include any private/sensitive information and you can disable this in settings at any time you want.", "unsupported_reason": { From d3b97ae91cdeb0369592895ab4d74673734a2ec2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 12:15:23 +0200 Subject: [PATCH 90/98] Add support for overriding sensor weight unit (#13897) --- src/panels/config/entities/entity-registry-settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index cba8909dde..f554e5bbf4 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -116,6 +116,7 @@ const OVERRIDE_SENSOR_UNITS = { speed: ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mph"], temperature: ["°C", "°F", "K"], volume: ["fl. oz.", "ft³", "gal", "L", "mL", "m³"], + weight: ["g", "kg", "lb", "mg", "oz", "µg"], }; const OVERRIDE_WEATHER_UNITS = { From f9d119d33d6507b84fff458766ca05bcab902d86 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 14:00:47 +0200 Subject: [PATCH 91/98] Add icons for new sensor device classes (#13895) --- src/common/const.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/common/const.ts b/src/common/const.ts index 19899e3033..e4e383e49b 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -6,6 +6,7 @@ import { mdiAlert, mdiAngleAcute, mdiAppleSafari, + mdiArrowLeftRight, mdiBell, mdiBookmark, mdiBrightness5, @@ -25,7 +26,6 @@ import { mdiFlower, mdiFormatListBulleted, mdiFormTextbox, - mdiGasCylinder, mdiGauge, mdiGestureTapButton, mdiGoogleAssistant, @@ -37,6 +37,8 @@ import { mdiLightningBolt, mdiMailbox, mdiMapMarkerRadius, + mdiMeterGas, + mdiMicrophoneMessage, mdiMolecule, mdiMoleculeCo, mdiMoleculeCo2, @@ -47,13 +49,14 @@ import { mdiRobotVacuum, mdiScriptText, mdiSineWave, - mdiMicrophoneMessage, + mdiSpeedometer, mdiThermometer, mdiThermostat, mdiTimerOutline, mdiVideo, mdiWaterPercent, mdiWeatherCloudy, + mdiWeight, mdiWhiteBalanceSunny, mdiWifi, } from "@mdi/js"; @@ -121,9 +124,10 @@ export const FIXED_DEVICE_CLASS_ICONS = { carbon_monoxide: mdiMoleculeCo, current: mdiCurrentAc, date: mdiCalendar, + distance: mdiArrowLeftRight, energy: mdiLightningBolt, frequency: mdiSineWave, - gas: mdiGasCylinder, + gas: mdiMeterGas, humidity: mdiWaterPercent, illuminance: mdiBrightness5, moisture: mdiWaterPercent, @@ -140,11 +144,14 @@ export const FIXED_DEVICE_CLASS_ICONS = { pressure: mdiGauge, reactive_power: mdiFlash, signal_strength: mdiWifi, + speed: mdiSpeedometer, sulphur_dioxide: mdiMolecule, temperature: mdiThermometer, timestamp: mdiClock, volatile_organic_compounds: mdiMolecule, voltage: mdiSineWave, + // volume: TBD, => no well matching icon found + weight: mdiWeight, }; /** Domains that have a state card. */ From 71c43058ea94864a43918ae2f7620d053b3df2a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 Sep 2022 14:51:41 +0200 Subject: [PATCH 92/98] Support displaying relative change in statistics graph cards (#13837) * Support displaying relative change in statistics graph cards * Address review comments * Add option to display state * Drop absolute sum option * Drop period if invalid * prevent fetching stats twice * Change stat_types to supported ones for statistics Co-authored-by: Bram Kragten --- src/components/chart/statistics-chart.ts | 20 +++++++---- src/data/recorder.ts | 2 +- .../cards/hui-statistics-graph-card.ts | 15 ++++---- .../hui-statistics-graph-card-editor.ts | 35 ++++++++++++++++--- src/translations/en.json | 4 ++- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index e5f60ad4e0..ea566c9fc7 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -30,6 +30,15 @@ import { import type { HomeAssistant } from "../../types"; import "./ha-chart-base"; +export type ExtendedStatisticType = StatisticType | "state"; + +export const statTypeMap: Record = { + mean: "mean", + min: "min", + max: "max", + sum: "sum", + state: "sum", +}; @customElement("statistics-chart") class StatisticsChart extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -42,7 +51,7 @@ class StatisticsChart extends LitElement { @property({ attribute: false }) public endTime?: Date; - @property({ type: Array }) public statTypes: Array = [ + @property({ type: Array }) public statTypes: Array = [ "sum", "min", "mean", @@ -307,7 +316,7 @@ class StatisticsChart extends LitElement { : this.statTypes; sortedTypes.forEach((type) => { - if (statisticsHaveType(stats, type)) { + if (statisticsHaveType(stats, statTypeMap[type])) { const band = drawBands && (type === "min" || type === "max"); statTypes.push(type); statDataSets.push({ @@ -335,7 +344,6 @@ class StatisticsChart extends LitElement { let prevDate: Date | null = null; // Process chart data. - let initVal: number | null = null; let prevSum: number | null = null; stats.forEach((stat) => { const date = new Date(stat.start); @@ -347,11 +355,11 @@ class StatisticsChart extends LitElement { statTypes.forEach((type) => { let val: number | null; if (type === "sum") { - if (initVal === null) { - initVal = val = stat.state || 0; + if (prevSum === null) { + val = 0; prevSum = stat.sum; } else { - val = initVal + ((stat.sum || 0) - prevSum!); + val = (stat.sum || 0) - prevSum; } } else { val = stat[type]; diff --git a/src/data/recorder.ts b/src/data/recorder.ts index f98c5b75a8..42f9ad5342 100644 --- a/src/data/recorder.ts +++ b/src/data/recorder.ts @@ -1,7 +1,7 @@ import { computeStateName } from "../common/entity/compute_state_name"; import { HomeAssistant } from "../types"; -export type StatisticType = "sum" | "min" | "max" | "mean"; +export type StatisticType = "state" | "sum" | "min" | "max" | "mean"; export interface Statistics { [statisticId: string]: StatisticValue[]; diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 4a6f4d39c7..47f737927a 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -38,8 +38,6 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { private _names: Record = {}; - private _fetching = false; - private _interval?: number; public disconnectedCallback() { @@ -92,7 +90,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { if (typeof config.stat_types === "string") { this._config = { ...config, stat_types: [config.stat_types] }; } else if (!config.stat_types) { - this._config = { ...config, stat_types: ["sum", "min", "max", "mean"] }; + this._config = { + ...config, + stat_types: ["state", "sum", "min", "max", "mean"], + }; } else { this._config = config; } @@ -156,15 +157,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { } private async _getStatistics(): Promise { - if (this._fetching) { - return; - } const startDate = new Date(); startDate.setTime( startDate.getTime() - 1000 * 60 * 60 * (24 * (this._config!.days_to_show || 30) + 1) ); - this._fetching = true; try { this._statistics = await fetchStatistics( this.hass!, @@ -173,8 +170,8 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { this._entities, this._config!.period ); - } finally { - this._fetching = false; + } catch (err) { + this._statistics = undefined; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts index 1dd5f3d8dc..606b1d2166 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistics-graph-card-editor.ts @@ -36,8 +36,11 @@ import { statisticsMetaHasType, } from "../../../../data/recorder"; import { deepEqual } from "../../../../common/util/deep-equal"; +import { statTypeMap } from "../../../../components/chart/statistics-chart"; +import { ensureArray } from "../../../../common/ensure-array"; const statTypeStruct = union([ + literal("state"), literal("sum"), literal("min"), literal("max"), @@ -64,7 +67,7 @@ const cardConfigStruct = assign( ); const periods = ["5minute", "hour", "day", "month"] as const; -const stat_types = ["mean", "min", "max", "sum"] as const; +const stat_types = ["mean", "min", "max", "sum", "state"] as const; @customElement("hui-statistics-graph-card-editor") export class HuiStatisticsGraphCardEditor @@ -156,7 +159,7 @@ export class HuiStatisticsGraphCardEditor disabled: !metaDatas || !metaDatas?.every((metaData) => - statisticsMetaHasType(metaData, stat_type) + statisticsMetaHasType(metaData, statTypeMap[stat_type]) ), })), }, @@ -190,7 +193,11 @@ export class HuiStatisticsGraphCardEditor ? Array.isArray(this._config!.stat_types) ? this._config!.stat_types : [this._config!.stat_types] - : stat_types; + : stat_types.filter((stat_type) => + this._metaDatas?.every((metaData) => + statisticsMetaHasType(metaData, statTypeMap[stat_type]) + ) + ); const data = { chart_type: "line", period: "hour", @@ -230,9 +237,27 @@ export class HuiStatisticsGraphCardEditor fireEvent(this, "config-changed", { config: ev.detail.value }); } - private _entitiesChanged(ev: CustomEvent): void { + private async _entitiesChanged(ev: CustomEvent): Promise { + const config = { ...this._config!, entities: ev.detail.value }; + if ( + config.entities?.some((statistic_id) => statistic_id.includes(":")) && + config.period === "5minute" + ) { + delete config.period; + } + if (config.stat_types && config.entities.length) { + const metadata = await getStatisticMetadata(this.hass!, config.entities); + config.stat_types = ensureArray(config.stat_types).filter((stat_type) => + metadata.every((metaData) => + statisticsMetaHasType(metaData, statTypeMap[stat_type]) + ) + ); + if (!config.stat_types.length) { + delete config.stat_types; + } + } fireEvent(this, "config-changed", { - config: { ...this._config!, entities: ev.detail.value }, + config, }); } diff --git a/src/translations/en.json b/src/translations/en.json index 9bca98f512..75e0a794af 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -517,6 +517,7 @@ "min": "min", "max": "max", "mean": "mean", + "state": "state", "sum": "sum" } }, @@ -3969,7 +3970,8 @@ "mean": "Mean", "min": "Min", "max": "Max", - "sum": "Sum" + "state": "State", + "sum": "Sum (change during period)" }, "chart_type": "Chart type", "periods": { From da38cbccf17da1c1befc6ce32cd0497c4c0f7444 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 Sep 2022 15:03:01 +0200 Subject: [PATCH 93/98] change interval if 5 min stats --- src/panels/lovelace/cards/hui-statistics-graph-card.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 47f737927a..73fe66c3e2 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -58,7 +58,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { clearInterval(this._interval); this._interval = window.setInterval( () => this._getStatistics(), - 1000 * 60 * 60 + this._intervalTimeout ); } @@ -126,7 +126,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { clearInterval(this._interval); this._interval = window.setInterval( () => this._getStatistics(), - 1000 * 60 * 60 + this._intervalTimeout ); } } @@ -156,6 +156,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { `; } + private get _intervalTimeout(): number { + return (this._config?.period === "5minute" ? 5 : 60) * 1000 * 60; + } + private async _getStatistics(): Promise { const startDate = new Date(); startDate.setTime( From dddb9225934d44c56dccc8d055c4198084c73bac Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 28 Sep 2022 15:05:50 +0200 Subject: [PATCH 94/98] Add Learn how it works link to cloud account (#13693) --- .../config/cloud/account/cloud-alexa-pref.ts | 114 +++++------- .../config/cloud/account/cloud-google-pref.ts | 176 +++++++----------- .../config/cloud/account/cloud-remote-pref.ts | 50 ++--- src/panels/config/cloud/alexa/cloud-alexa.ts | 172 ++++++++++------- .../cloud-google-assistant.ts | 77 ++++++-- src/translations/en.json | 23 ++- 6 files changed, 321 insertions(+), 291 deletions(-) diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index aaae98dd20..ad0ae9a406 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -1,11 +1,13 @@ import "@material/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property, state } from "lit/decorators"; +import { property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; import "../../../../components/ha-card"; +import "../../../../components/ha-settings-row"; import "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch"; -import { syncCloudAlexaEntities } from "../../../../data/alexa"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import type { HomeAssistant } from "../../../../types"; @@ -14,8 +16,6 @@ export class CloudAlexaPref extends LitElement { @property() public cloudStatus?: CloudStatusLoggedIn; - @state() private _syncing = false; - protected render(): TemplateResult { if (!this.cloudStatus) { return html``; @@ -31,7 +31,20 @@ export class CloudAlexaPref extends LitElement { "ui.panel.config.cloud.account.alexa.title" )} > -
    +
    + + + ` : html` -
    -

    + + ${this.hass!.localize( "ui.panel.config.cloud.account.alexa.enable_state_reporting" )} -

    -
    - -
    -
    -

    - ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.info_state_reporting" - )} -

    + + + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.info_state_reporting" + )} + + + `}
    - ${alexa_registered - ? html` - - ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.sync_entities" - )} - - ` - : ""} -
    ${this.hass!.localize( @@ -127,21 +125,6 @@ export class CloudAlexaPref extends LitElement { `; } - private async _handleSync() { - this._syncing = true; - try { - await syncCloudAlexaEntities(this.hass!); - } catch (err: any) { - alert( - `${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.sync_entities_error" - )} ${err.body.message}` - ); - } finally { - this._syncing = false; - } - } - private async _enabledToggleChanged(ev) { const toggle = ev.target as HaSwitch; try { @@ -180,40 +163,33 @@ export class CloudAlexaPref extends LitElement { a { color: var(--primary-color); } - .switch { + ha-settings-row { + padding: 0; + } + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } .card-actions { display: flex; } .card-actions a { text-decoration: none; } - .spacer { - flex-grow: 1; - } - .state-reporting { - display: flex; - margin-top: 1.5em; - } - .state-reporting + p { - margin-top: 0.5em; - } - .state-reporting h3 { - flex-grow: 1; - margin: 0; - } - .state-reporting-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } `; } } diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index 16e831e448..bef63407fa 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -1,15 +1,15 @@ import "@material/mwc-button"; +import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { property, state } from "lit/decorators"; +import { property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-card"; +import "../../../../components/ha-settings-row"; import type { HaSwitch } from "../../../../components/ha-switch"; import "../../../../components/ha-textfield"; import type { HaTextField } from "../../../../components/ha-textfield"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; -import { syncCloudGoogleEntities } from "../../../../data/google_assistant"; -import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; @@ -18,8 +18,6 @@ export class CloudGooglePref extends LitElement { @property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn; - @state() private _syncing = false; - protected render(): TemplateResult { if (!this.cloudStatus) { return html``; @@ -36,11 +34,23 @@ export class CloudGooglePref extends LitElement { "ui.panel.config.cloud.account.google.title" )} > -
    +
    @@ -110,61 +120,50 @@ export class CloudGooglePref extends LitElement { ` : ""} -
    -

    - ${this.hass.localize( + + + ${this.hass!.localize( "ui.panel.config.cloud.account.google.enable_state_reporting" )} -

    -
    - -
    -
    -

    - ${this.hass.localize( - "ui.panel.config.cloud.account.google.info_state_reporting" - )} -

    -
    -

    + + + ${this.hass!.localize( + "ui.panel.config.cloud.account.google.info_state_reporting" + )} + + + + + + ${this.hass.localize( "ui.panel.config.cloud.account.google.security_devices" )} -

    - ${this.hass.localize( - "ui.panel.config.cloud.account.google.enter_pin_info" + + + ${this.hass.localize( + "ui.panel.config.cloud.account.google.enter_pin_info" + )} + + + + -
    + .placeholder=${this.hass.localize( + "ui.panel.config.cloud.account.google.enter_pin_hint" + )} + .value=${google_secure_devices_pin || ""} + @change=${this._pinChanged} + > `}
    - ${google_registered - ? html` - - ${this.hass.localize( - "ui.panel.config.cloud.account.google.sync_entities" - )} - - ` - : ""} -
    ${this.hass.localize( @@ -177,32 +176,7 @@ export class CloudGooglePref extends LitElement { `; } - private async _handleSync() { - this._syncing = true; - try { - await syncCloudGoogleEntities(this.hass!); - } catch (err: any) { - showAlertDialog(this, { - title: this.hass.localize( - `ui.panel.config.cloud.account.google.${ - err.status_code === 404 - ? "not_configured_title" - : "sync_failed_title" - }` - ), - text: this.hass.localize( - `ui.panel.config.cloud.account.google.${ - err.status_code === 404 ? "not_configured_text" : "sync_failed_text" - }` - ), - }); - fireEvent(this, "ha-refresh-cloud-status"); - } finally { - this._syncing = false; - } - } - - private async _enableToggleChanged(ev) { + private async _enabledToggleChanged(ev) { const toggle = ev.target as HaSwitch; try { await updateCloudPref(this.hass, { [toggle.id]: toggle.checked! }); @@ -252,15 +226,27 @@ export class CloudGooglePref extends LitElement { a { color: var(--primary-color); } - .switch { + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } + ha-settings-row { + padding: 0; + } ha-textfield { width: 250px; display: block; @@ -275,32 +261,6 @@ export class CloudGooglePref extends LitElement { .warning { color: var(--error-color); } - .secure_devices { - padding-top: 8px; - } - .spacer { - flex-grow: 1; - } - - .state-reporting { - display: flex; - margin-top: 1.5em; - } - .state-reporting + p { - margin-top: 0.5em; - } - h3 { - margin: 0 0 8px 0; - } - .state-reporting h3 { - flex-grow: 1; - margin: 0; - } - .state-reporting-switch { - margin-top: 0.25em; - margin-right: 7px; - margin-left: 0.5em; - } `; } } diff --git a/src/panels/config/cloud/account/cloud-remote-pref.ts b/src/panels/config/cloud/account/cloud-remote-pref.ts index d195b49890..bc27ede2f6 100644 --- a/src/panels/config/cloud/account/cloud-remote-pref.ts +++ b/src/panels/config/cloud/account/cloud-remote-pref.ts @@ -1,5 +1,5 @@ import "@material/mwc-button"; -import { mdiContentCopy } from "@mdi/js"; +import { mdiContentCopy, mdiHelpCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -58,12 +58,26 @@ export class CloudRemotePref extends LitElement { "ui.panel.config.cloud.account.remote.title" )} > -
    + +
    ${!remote_connected && remote_enabled ? html` @@ -98,18 +112,6 @@ export class CloudRemotePref extends LitElement { >
    - - ${this.hass.localize( - "ui.panel.config.cloud.account.remote.link_learn_how_it_works" - )} - -
    ${this.hass.localize( "ui.panel.config.cloud.account.remote.certificate_info" @@ -158,15 +160,24 @@ export class CloudRemotePref extends LitElement { a { color: var(--primary-color); } - .switch { + .header-actions { position: absolute; right: 24px; top: 24px; + display: flex; + flex-direction: row; } - :host([dir="rtl"]) .switch { + :host([dir="rtl"]) .header-actions { right: auto; left: 24px; } + .header-actions .icon-link { + margin-top: -16px; + margin-inline-end: 8px; + margin-right: 8px; + direction: var(--direction); + color: var(--secondary-text-color); + } .warning { font-weight: bold; margin-bottom: 1em; @@ -179,19 +190,12 @@ export class CloudRemotePref extends LitElement { right: 24px; top: 24px; } - :host([dir="rtl"]) .switch { - right: auto; - left: 24px; - } .card-actions { display: flex; } .card-actions a { text-decoration: none; } - .spacer { - flex-grow: 1; - } ha-svg-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color); diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index d388466f8d..0ac078e360 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -5,6 +5,9 @@ import { mdiCheckboxMultipleMarked, mdiCloseBox, mdiCloseBoxMultiple, + mdiDotsVertical, + mdiFormatListChecks, + mdiSync, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -26,7 +29,11 @@ import "../../../../components/ha-card"; import "../../../../components/ha-formfield"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-switch"; -import { AlexaEntity, fetchCloudAlexaEntities } from "../../../../data/alexa"; +import { + AlexaEntity, + fetchCloudAlexaEntities, + syncCloudAlexaEntities, +} from "../../../../data/alexa"; import { AlexaEntityConfig, CloudPreferences, @@ -59,6 +66,8 @@ class CloudAlexa extends SubscribeMixin(LitElement) { @state() private _entities?: AlexaEntity[]; + @state() private _syncing = false; + @state() private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {}; @@ -222,74 +231,84 @@ class CloudAlexa extends SubscribeMixin(LitElement) { } return html` - - ${ - emptyFilter - ? html` - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.manage_defaults" - )} - ` - : "" - } - ${ - !emptyFilter - ? html` - - ` - : "" - } - ${ - exposedCards.length > 0 - ? html` -
    -

    - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed_entities" - )} -

    - ${!this.narrow - ? this.hass!.localize( - "ui.panel.config.cloud.alexa.exposed", - "selected", - selected - ) - : selected} -
    -
    ${exposedCards}
    - ` - : "" - } - ${ - notExposedCards.length > 0 - ? html` -
    -

    - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed_entities" - )} -

    - ${!this.narrow - ? this.hass!.localize( - "ui.panel.config.cloud.alexa.not_exposed", - "selected", - this._entities.length - selected - ) - : this._entities.length - selected} -
    -
    ${notExposedCards}
    - ` - : "" - } -
    + + + + + + ${this.hass.localize("ui.panel.config.cloud.alexa.manage_defaults")} + + + + + ${this.hass.localize("ui.panel.config.cloud.alexa.sync_entities")} + + + + ${!emptyFilter + ? html` + + ` + : ""} + ${exposedCards.length > 0 + ? html` +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.exposed", + "selected", + selected + ) + : selected} +
    +
    ${exposedCards}
    + ` + : ""} + ${notExposedCards.length > 0 + ? html` +
    +

    + ${this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed_entities" + )} +

    + ${!this.narrow + ? this.hass!.localize( + "ui.panel.config.cloud.alexa.not_exposed", + "selected", + this._entities.length - selected + ) + : this._entities.length - selected} +
    +
    ${notExposedCards}
    + ` + : ""}
    `; } @@ -423,6 +442,21 @@ class CloudAlexa extends SubscribeMixin(LitElement) { }); } + private async _handleSync() { + this._syncing = true; + try { + await syncCloudAlexaEntities(this.hass!); + } catch (err: any) { + alert( + `${this.hass!.localize( + "ui.panel.config.cloud.alexa.sync_entities_error" + )} ${err.body.message}` + ); + } finally { + this._syncing = false; + } + } + private async _updateDomainExposed(domain: string, expose: boolean) { const defaultExpose = this.cloudStatus.prefs.alexa_default_expose || diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index f177094f8a..da22c0fc0b 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -5,6 +5,9 @@ import { mdiCheckboxMultipleMarked, mdiCloseBox, mdiCloseBoxMultiple, + mdiDotsVertical, + mdiFormatListChecks, + mdiSync, } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -45,6 +48,7 @@ import { GoogleEntity, } from "../../../../data/google_assistant"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; @@ -64,6 +68,8 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { @state() private _entities?: GoogleEntity[]; + @state() private _syncing = false; + @state() private _entityConfigs: CloudPreferences["google_entity_configs"] = {}; @@ -249,19 +255,39 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { .hass=${this.hass} .header=${this.hass!.localize("ui.panel.config.cloud.google.title")} .narrow=${this.narrow}> - ${ - emptyFilter - ? html` - ${this.hass!.localize( - "ui.panel.config.cloud.google.manage_defaults" - )} - ` - : "" - } + + + + + ${this.hass.localize( + "ui.panel.config.cloud.google.manage_defaults" + )} + + + + + ${this.hass.localize("ui.panel.config.cloud.google.sync_entities")} + + + ${ !emptyFilter ? html` @@ -506,6 +532,31 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) { ); } + private async _handleSync() { + this._syncing = true; + try { + await cloudSyncGoogleAssistant(this.hass!); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + `ui.panel.config.cloud.google.${ + err.status_code === 404 + ? "not_configured_title" + : "sync_failed_title" + }` + ), + text: this.hass.localize( + `ui.panel.config.cloud.google.${ + err.status_code === 404 ? "not_configured_text" : "sync_failed_text" + }` + ), + }); + fireEvent(this, "ha-refresh-cloud-status"); + } finally { + this._syncing = false; + } + } + private _ensureEntitySync() { if (this._popstateSyncAttached) { return; diff --git a/src/translations/en.json b/src/translations/en.json index 75e0a794af..5bbe3ca627 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2494,14 +2494,13 @@ "config_documentation": "Configuration documentation", "enable_state_reporting": "Enable State Reporting", "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.", - "sync_entities": "Sync Entities to Amazon", - "manage_entities": "Manage Entities", - "sync_entities_error": "Failed to sync entities:", "state_reporting_error": "Unable to {enable_disable} report state.", + "manage_entities": "Manage Entities", "enable": "enable", "disable": "disable", "not_configured_title": "Alexa is not activated", - "not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app." + "not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app.", + "link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]" }, "google": { "title": "Google Assistant", @@ -2516,13 +2515,11 @@ "enter_pin_info": "Please enter a PIN to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this PIN when interacting with such devices via Google Assistant.", "devices_pin": "Security Devices PIN", "enter_pin_hint": "Enter a PIN to use security devices", - "sync_entities": "Sync Entities to Google", "manage_entities": "Manage Entities", "enter_pin_error": "Unable to store PIN:", "not_configured_title": "Google Assistant is not activated", "not_configured_text": "Before you can use Google Assistant, you need to activate the Home Assistant Cloud skill for Google Assistant in the Google Home app.", - "sync_failed_title": "Syncing failed", - "sync_failed_text": "Syncing your entities failed, try again or check the logs." + "link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]" }, "webhooks": { "title": "Webhooks", @@ -2549,7 +2546,9 @@ "follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]", "exposed": "[%key:ui::panel::config::cloud::google::exposed%]", "not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]", - "expose": "Expose to Alexa" + "expose": "Expose to Alexa", + "sync_entities": "Synchronize entities", + "sync_entities_error": "Failed to sync entities:" }, "dialog_certificate": { "certificate_information": "Certificate Information", @@ -2572,7 +2571,13 @@ "follow_domain": "Follow domain", "exposed": "{selected} exposed", "not_exposed": "{selected} not exposed", - "sync_to_google": "Synchronizing changes to Google." + "sync_to_google": "Synchronizing changes to Google.", + "sync_entities": "Synchronize entities", + "sync_entities_error": "Failed to sync entities:", + "not_configured_title": "[%key:ui::panel::config::cloud::account::google::not_configured_title%]", + "not_configured_text": "[%key:ui::panel::config::cloud::account::google::not_configured_text%]", + "sync_failed_title": "Syncing failed", + "sync_failed_text": "Syncing your entities failed, try again or check the logs." }, "dialog_cloudhook": { "webhook_for": "Webhook for {name}", From 8e4bebb694ece6dd6eb68d92ced01c32189dc3d8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 Sep 2022 17:39:40 +0200 Subject: [PATCH 95/98] Integrations v2 (#13887) * WIP: Integrations v2 * update * manifests * update wording * show yaml only * Show spinner * Update * Use virtulizer * Update * change interval if 5 min stats * remove yaml * fix application credentials * Add zwave and zigbee device support * make back button bigger * margin --- .../integrations/protocolIntegrationPicked.ts | 10 +- src/components/ha-dialog.ts | 2 +- src/data/integrations.ts | 37 + src/data/supported_brands.ts | 9 +- .../config-flow/dialog-data-entry-flow.ts | 160 ++--- .../config-flow/show-dialog-config-flow.ts | 12 - .../show-dialog-data-entry-flow.ts | 2 - src/dialogs/config-flow/step-flow-abort.ts | 9 +- .../config-flow/step-flow-pick-flow.ts | 130 ---- .../config-flow/step-flow-pick-handler.ts | 372 ---------- .../integrations/dialog-add-integration.ts | 656 ++++++++++++++++++ .../integrations/ha-config-integrations.ts | 27 +- .../integrations/ha-domain-integrations.ts | 225 ++++++ .../integrations/ha-integration-list-item.ts | 151 ++++ .../show-add-integration-dialog.ts | 12 + src/translations/en.json | 8 +- 16 files changed, 1162 insertions(+), 660 deletions(-) create mode 100644 src/data/integrations.ts delete mode 100644 src/dialogs/config-flow/step-flow-pick-flow.ts delete mode 100644 src/dialogs/config-flow/step-flow-pick-handler.ts create mode 100644 src/panels/config/integrations/dialog-add-integration.ts create mode 100644 src/panels/config/integrations/ha-domain-integrations.ts create mode 100644 src/panels/config/integrations/ha-integration-list-item.ts create mode 100644 src/panels/config/integrations/show-add-integration-dialog.ts diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index ff62cacbd9..4af63a34b1 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -1,11 +1,11 @@ import { html } from "lit"; import { getConfigEntries } from "../../data/config_entries"; +import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { isComponentLoaded } from "../config/is_component_loaded"; -import { fireEvent } from "../dom/fire_event"; import { navigate } from "../navigate"; export const protocolIntegrationPicked = async ( @@ -39,8 +39,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zwave_js", + showConfigFlowDialog(element, { + startFlowHandler: "zwave_js", }); }, }); @@ -75,8 +75,8 @@ export const protocolIntegrationPicked = async ( "ui.panel.config.integrations.config_flow.proceed" ), confirm: () => { - fireEvent(element, "handler-picked", { - handler: "zha", + showConfigFlowDialog(element, { + startFlowHandler: "zha", }); }, }); diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index b5b27e911f..3e8dba0946 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -91,7 +91,7 @@ export class HaDialog extends DialogBase { .header_button { position: absolute; right: 16px; - top: 10px; + top: 14px; text-decoration: none; color: inherit; } diff --git a/src/data/integrations.ts b/src/data/integrations.ts new file mode 100644 index 0000000000..70c799b154 --- /dev/null +++ b/src/data/integrations.ts @@ -0,0 +1,37 @@ +import { HomeAssistant } from "../types"; + +export type IotStandards = "z-wave" | "zigbee" | "homekit" | "matter"; + +export interface Integration { + name?: string; + config_flow?: boolean; + integrations?: Integrations; + iot_standards?: IotStandards[]; + is_built_in?: boolean; + iot_class?: string; +} + +export interface Integrations { + [domain: string]: Integration; +} + +export interface IntegrationDescriptions { + core: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + translated_name: string[]; + }; + custom: { + integration: Integrations; + hardware: Integrations; + helper: Integrations; + }; +} + +export const getIntegrationDescriptions = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "integration/descriptions", + }); diff --git a/src/data/supported_brands.ts b/src/data/supported_brands.ts index 8f0afcf884..62691ed275 100644 --- a/src/data/supported_brands.ts +++ b/src/data/supported_brands.ts @@ -1,6 +1,13 @@ -import { SupportedBrandObj } from "../dialogs/config-flow/step-flow-pick-handler"; import type { HomeAssistant } from "../types"; +export interface SupportedBrandObj { + name: string; + slug: string; + is_add?: boolean; + is_helper?: boolean; + supported_flows: string[]; +} + export type SupportedBrandHandler = Record; export const getSupportedBrands = (hass: HomeAssistant) => diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 7654a0b506..3c148b7172 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -18,9 +18,7 @@ import { AreaRegistryEntry, subscribeAreaRegistry, } from "../../data/area_registry"; -import { fetchConfigFlowInProgress } from "../../data/config_flow"; import { - DataEntryFlowProgress, DataEntryFlowStep, subscribeDataEntryFlowProgressed, } from "../../data/data_entry_flow"; @@ -28,14 +26,12 @@ import { DeviceRegistryEntry, subscribeDeviceRegistry, } from "../../data/device_registry"; -import { fetchIntegrationManifest } from "../../data/integration"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { showAlertDialog } from "../generic/show-dialog-box"; import { DataEntryFlowDialogParams, - FlowHandlers, LoadingReason, } from "./show-dialog-data-entry-flow"; import "./step-flow-abort"; @@ -44,8 +40,6 @@ import "./step-flow-external"; import "./step-flow-form"; import "./step-flow-loading"; import "./step-flow-menu"; -import "./step-flow-pick-flow"; -import "./step-flow-pick-handler"; import "./step-flow-progress"; let instance = 0; @@ -86,12 +80,8 @@ class DataEntryFlowDialog extends LitElement { @state() private _areas?: AreaRegistryEntry[]; - @state() private _handlers?: FlowHandlers; - @state() private _handler?: string; - @state() private _flowsInProgress?: DataEntryFlowProgress[]; - private _unsubAreas?: UnsubscribeFunc; private _unsubDevices?: UnsubscribeFunc; @@ -102,15 +92,39 @@ class DataEntryFlowDialog extends LitElement { this._params = params; this._instance = instance++; - if (params.startFlowHandler) { - this._checkFlowsInProgress(params.startFlowHandler); - return; - } + const curInstance = this._instance; + let step: DataEntryFlowStep; - if (params.continueFlowId) { + if (params.startFlowHandler) { + this._loading = "loading_flow"; + this._handler = params.startFlowHandler; + try { + step = await this._params!.flowConfig.createFlow( + this.hass, + params.startFlowHandler + ); + } catch (err: any) { + this.closeDialog(); + let message = err.message || err.body || "Unknown error"; + if (typeof message !== "string") { + message = JSON.stringify(message); + } + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.error" + ), + text: `${this.hass.localize( + "ui.panel.config.integrations.config_flow.could_not_load" + )}: ${message}`, + }); + return; + } + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; + } + } else if (params.continueFlowId) { this._loading = "loading_flow"; - const curInstance = this._instance; - let step: DataEntryFlowStep; try { step = await params.flowConfig.fetchFlow( this.hass, @@ -132,32 +146,17 @@ class DataEntryFlowDialog extends LitElement { }); return; } - - // Happens if second showDialog called - if (curInstance !== this._instance) { - return; - } - - this._processStep(step); - this._loading = undefined; + } else { return; } - // Create a new config flow. Show picker - if (!params.flowConfig.getFlowHandlers) { - throw new Error("No getFlowHandlers defined in flow config"); + // Happens if second showDialog called + if (curInstance !== this._instance) { + return; } - this._step = null; - // We only load the handlers once - if (this._handlers === undefined) { - this._loading = "loading_handlers"; - try { - this._handlers = await params.flowConfig.getFlowHandlers(this.hass); - } finally { - this._loading = undefined; - } - } + this._processStep(step); + this._loading = undefined; } public closeDialog() { @@ -185,7 +184,6 @@ class DataEntryFlowDialog extends LitElement { this._step = undefined; this._params = undefined; this._devices = undefined; - this._flowsInProgress = undefined; this._handler = undefined; if (this._unsubAreas) { this._unsubAreas(); @@ -218,15 +216,12 @@ class DataEntryFlowDialog extends LitElement { hideActions >
    - ${this._loading || - (this._step === null && - this._handlers === undefined && - this._handler === undefined) + ${this._loading || this._step === null ? html` @@ -273,24 +268,7 @@ class DataEntryFlowDialog extends LitElement { dialogAction="close" >
    - ${this._step === null - ? this._handler - ? html`` - : // Show handler picker - html` - - ` - : this._step.type === "form" + ${this._step.type === "form" ? html` flow.handler === handler); - - if (!flowsInProgress.length) { - // No flows in progress, create a new flow - this._loading = "loading_flow"; - let step: DataEntryFlowStep; - try { - step = await this._params!.flowConfig.createFlow(this.hass, handler); - } catch (err: any) { - this.closeDialog(); - const message = - err?.status_code === 404 - ? this.hass.localize( - "ui.panel.config.integrations.config_flow.no_config_flow" - ) - : `${this.hass.localize( - "ui.panel.config.integrations.config_flow.could_not_load" - )}: ${err?.body?.message || err?.message}`; - - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.error" - ), - text: message, - }); - return; - } finally { - this._handler = undefined; - } - this._processStep(step); - if (this._params!.manifest === undefined) { - try { - this._params!.manifest = await fetchIntegrationManifest( - this.hass, - this._params?.domain || step.handler - ); - } catch (_) { - // No manifest - this._params!.manifest = null; - } - } - } else { - this._step = null; - this._flowsInProgress = flowsInProgress; - } - this._loading = undefined; - } - - private _handlerPicked(ev) { - this._checkFlowsInProgress(ev.detail.handler); - } - private async _processStep( step: DataEntryFlowStep | undefined | Promise ): Promise { diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index c1d56c6d73..84d332492c 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -3,11 +3,9 @@ import { createConfigFlow, deleteConfigFlow, fetchConfigFlow, - getConfigFlowHandlers, handleConfigFlowStep, } from "../../data/config_flow"; import { domainToName } from "../../data/integration"; -import { getSupportedBrands } from "../../data/supported_brands"; import { DataEntryFlowDialogParams, loadDataEntryFlowDialog, @@ -22,16 +20,6 @@ export const showConfigFlowDialog = ( ): void => showFlowDialog(element, dialogParams, { loadDevicesAndAreas: true, - getFlowHandlers: async (hass) => { - const [integrations, helpers, supportedBrands] = await Promise.all([ - getConfigFlowHandlers(hass, "integration"), - getConfigFlowHandlers(hass, "helper"), - getSupportedBrands(hass), - hass.loadBackendTranslation("title", undefined, true), - ]); - - return { integrations, helpers, supportedBrands }; - }, createFlow: async (hass, handler) => { const [step] = await Promise.all([ createConfigFlow(hass, handler), diff --git a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts index 1c5f407c69..5a6239cb3b 100644 --- a/src/dialogs/config-flow/show-dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/show-dialog-data-entry-flow.ts @@ -22,8 +22,6 @@ export interface FlowHandlers { export interface FlowConfig { loadDevicesAndAreas: boolean; - getFlowHandlers?: (hass: HomeAssistant) => Promise; - createFlow(hass: HomeAssistant, handler: string): Promise; fetchFlow(hass: HomeAssistant, flowId: string): Promise; diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index e1909d0bfe..08b8a46df1 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -12,10 +12,10 @@ import { DataEntryFlowStepAbort } from "../../data/data_entry_flow"; import { HomeAssistant } from "../../types"; import { showAddApplicationCredentialDialog } from "../../panels/config/application_credentials/show-dialog-add-application-credential"; import { configFlowContentStyles } from "./styles"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { domainToName } from "../../data/integration"; import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import { showConfigFlowDialog } from "./show-dialog-config-flow"; +import { domainToName } from "../../data/integration"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("step-flow-abort") class StepFlowAbort extends LitElement { @@ -56,11 +56,16 @@ class StepFlowAbort extends LitElement { private async _handleMissingCreds() { const confirm = await showConfirmationDialog(this, { title: this.hass.localize( + "ui.panel.config.integrations.config_flow.missing_credentials_title" + ), + text: this.hass.localize( "ui.panel.config.integrations.config_flow.missing_credentials", { integration: domainToName(this.hass.localize, this.domain), } ), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), }); this._flowDone(); if (!confirm) { diff --git a/src/dialogs/config-flow/step-flow-pick-flow.ts b/src/dialogs/config-flow/step-flow-pick-flow.ts deleted file mode 100644 index fd24243a0b..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-flow.ts +++ /dev/null @@ -1,130 +0,0 @@ -import "@polymer/paper-item"; -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-icon-next"; -import { localizeConfigFlowTitle } from "../../data/config_flow"; -import { DataEntryFlowProgress } from "../../data/data_entry_flow"; -import { domainToName } from "../../data/integration"; -import { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { FlowConfig } from "./show-dialog-data-entry-flow"; -import { configFlowContentStyles } from "./styles"; - -@customElement("step-flow-pick-flow") -class StepFlowPickFlow extends LitElement { - public flowConfig!: FlowConfig; - - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) - public flowsInProgress!: DataEntryFlowProgress[]; - - @property() public handler!: string; - - protected render(): TemplateResult { - return html` -

    - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.title" - )} -

    - -
    - ${this.flowsInProgress.map( - (flow) => html` - - - - ${localizeConfigFlowTitle(this.hass.localize, flow)} - - - ` - )} - - - ${this.hass.localize( - "ui.panel.config.integrations.config_flow.pick_flow_step.new_flow", - "integration", - domainToName(this.hass.localize, this.handler) - )} - - - -
    - `; - } - - private _startNewFlowPicked(ev) { - this._startFlow(ev.currentTarget.handler); - } - - private _startFlow(handler: string) { - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.createFlow(this.hass, handler), - }); - } - - private _flowInProgressPicked(ev) { - const flow: DataEntryFlowProgress = ev.currentTarget.flow; - fireEvent(this, "flow-update", { - stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id), - }); - } - - static get styles(): CSSResultGroup { - return [ - configFlowContentStyles, - css` - img { - width: 40px; - height: 40px; - } - ha-icon-next { - margin-right: 8px; - } - div { - overflow: auto; - max-height: 600px; - margin: 16px 0; - } - h2 { - padding-inline-end: 66px; - direction: var(--direction); - } - @media all and (max-height: 900px) { - div { - max-height: calc(100vh - 134px); - } - } - paper-icon-item, - paper-item { - cursor: pointer; - margin-bottom: 4px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "step-flow-pick-flow": StepFlowPickFlow; - } -} diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts deleted file mode 100644 index 21fa545da3..0000000000 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ /dev/null @@ -1,372 +0,0 @@ -import "@material/mwc-list/mwc-list"; -import "@material/mwc-list/mwc-list-item"; -import Fuse from "fuse.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { fireEvent } from "../../common/dom/fire_event"; -import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked"; -import { navigate } from "../../common/navigate"; -import { caseInsensitiveStringCompare } from "../../common/string/compare"; -import { LocalizeFunc } from "../../common/translations/localize"; -import "../../components/ha-icon-next"; -import "../../components/search-input"; -import { domainToName } from "../../data/integration"; -import { haStyleScrollbar } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; -import { documentationUrl } from "../../util/documentation-url"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import { FlowHandlers } from "./show-dialog-data-entry-flow"; -import { configFlowContentStyles } from "./styles"; - -interface HandlerObj { - name: string; - slug: string; - is_add?: boolean; - is_helper?: boolean; -} - -export interface SupportedBrandObj extends HandlerObj { - supported_flows: string[]; -} - -declare global { - // for fire event - interface HASSDomEvents { - "handler-picked": { - handler: string; - }; - } -} - -@customElement("step-flow-pick-handler") -class StepFlowPickHandler extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public handlers!: FlowHandlers; - - @property() public initialFilter?: string; - - @state() private _filter?: string; - - private _width?: number; - - private _height?: number; - - private _filterHandlers = memoizeOne( - ( - h: FlowHandlers, - filter?: string, - _localize?: LocalizeFunc - ): [(HandlerObj | SupportedBrandObj)[], HandlerObj[]] => { - const integrations: (HandlerObj | SupportedBrandObj)[] = - h.integrations.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - })); - - for (const [domain, domainBrands] of Object.entries(h.supportedBrands)) { - for (const [slug, name] of Object.entries(domainBrands)) { - integrations.push({ - slug, - name, - supported_flows: [domain], - }); - } - } - - if (filter) { - const options: Fuse.IFuseOptions = { - keys: ["name", "slug"], - isCaseSensitive: false, - minMatchCharLength: 2, - threshold: 0.2, - }; - const helpers: HandlerObj[] = h.helpers.map((handler) => ({ - name: domainToName(this.hass.localize, handler), - slug: handler, - is_helper: true, - })); - return [ - new Fuse(integrations, options) - .search(filter) - .map((result) => result.item), - new Fuse(helpers, options) - .search(filter) - .map((result) => result.item), - ]; - } - return [ - integrations.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name) - ), - [], - ]; - } - ); - - protected render(): TemplateResult { - const [integrations, helpers] = this._getHandlers(); - - const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"] - .filter((domain) => isComponentLoaded(this.hass, domain)) - .map((domain) => ({ - name: this.hass.localize( - `ui.panel.config.integrations.add_${domain}_device` - ), - slug: domain, - is_add: true, - })) - .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); - - return html` -

    ${this.hass.localize("ui.panel.config.integrations.new")}

    - - - ${addDeviceRows.length - ? html` - ${addDeviceRows.map((handler) => this._renderRow(handler))} - - ` - : ""} - ${integrations.length - ? integrations.map((handler) => this._renderRow(handler)) - : html` -

    - ${this.hass.localize( - "ui.panel.config.integrations.note_about_integrations" - )}
    - ${this.hass.localize( - "ui.panel.config.integrations.note_about_website_reference" - )}${this.hass.localize( - "ui.panel.config.integrations.home_assistant_website" - )}. -

    - `} - ${helpers.length - ? html` - - ${helpers.map((handler) => this._renderRow(handler))} - ` - : ""} -
    - `; - } - - private _renderRow(handler: HandlerObj) { - return html` - - - ${handler.name} ${handler.is_helper ? " (helper)" : ""} - ${handler.is_add ? "" : html``} - - `; - } - - public willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - if (this._filter === undefined && this.initialFilter !== undefined) { - this._filter = this.initialFilter; - } - if (this.initialFilter !== undefined && this._filter === "") { - this.initialFilter = undefined; - this._filter = ""; - this._width = undefined; - this._height = undefined; - } else if ( - this.hasUpdated && - changedProps.has("_filter") && - (!this._width || !this._height) - ) { - // Store the width and height so that when we search, box doesn't jump - const boundingRect = - this.shadowRoot!.querySelector("mwc-list")!.getBoundingClientRect(); - this._width = boundingRect.width; - this._height = boundingRect.height; - } - } - - protected firstUpdated(changedProps) { - super.firstUpdated(changedProps); - setTimeout( - () => this.shadowRoot!.querySelector("search-input")!.focus(), - 0 - ); - } - - private _getHandlers() { - return this._filterHandlers( - this.handlers, - this._filter, - this.hass.localize - ); - } - - private async _filterChanged(e) { - this._filter = e.detail.value; - } - - private async _handlerPicked(ev) { - const handler: HandlerObj | SupportedBrandObj = ev.currentTarget.handler; - - if (handler.is_add) { - this._handleAddPicked(handler.slug); - return; - } - - if (handler.is_helper) { - navigate(`/config/helpers/add?domain=${handler.slug}`); - // This closes dialog. - fireEvent(this, "flow-update"); - return; - } - - if ("supported_flows" in handler) { - const slug = handler.supported_flows[0]; - - showConfirmationDialog(this, { - text: this.hass.localize( - "ui.panel.config.integrations.config_flow.supported_brand_flow", - { - supported_brand: handler.name, - flow_domain_name: domainToName(this.hass.localize, slug), - } - ), - confirm: () => { - if (["zha", "zwave_js"].includes(slug)) { - this._handleAddPicked(slug); - return; - } - - fireEvent(this, "handler-picked", { - handler: slug, - }); - }, - }); - - return; - } - - fireEvent(this, "handler-picked", { - handler: handler.slug, - }); - } - - private async _handleAddPicked(slug: string): Promise { - await protocolIntegrationPicked(this, this.hass, slug); - // This closes dialog. - fireEvent(this, "flow-update"); - } - - private _maybeSubmit(ev: KeyboardEvent) { - if (ev.key !== "Enter") { - return; - } - - const handlers = this._getHandlers(); - - if (handlers.length > 0) { - fireEvent(this, "handler-picked", { - handler: handlers[0][0].slug, - }); - } - } - - static get styles(): CSSResultGroup { - return [ - configFlowContentStyles, - haStyleScrollbar, - css` - img { - width: 40px; - height: 40px; - } - search-input { - display: block; - margin: 16px 16px 0; - } - ha-icon-next { - margin-right: 8px; - } - mwc-list { - overflow: auto; - max-height: 600px; - } - .divider { - border-bottom-color: var(--divider-color); - } - h2 { - padding-inline-end: 66px; - direction: var(--direction); - } - @media all and (max-height: 900px) { - mwc-list { - max-height: calc(100vh - 134px); - } - } - p { - text-align: center; - padding: 16px; - margin: 0; - } - p > a { - color: var(--primary-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "step-flow-pick-handler": StepFlowPickHandler; - } -} diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts new file mode 100644 index 0000000000..474bbfa57a --- /dev/null +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -0,0 +1,656 @@ +import "@material/mwc-button"; +import "@material/mwc-list/mwc-list"; +import Fuse from "fuse.js"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { navigate } from "../../../common/navigate"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-icon-button-prev"; +import "../../../components/search-input"; +import { fetchConfigFlowInProgress } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { + getIntegrationDescriptions, + Integrations, +} from "../../../data/integrations"; +import { + getSupportedBrands, + SupportedBrandHandler, +} from "../../../data/supported_brands"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { haStyleDialog, haStyleScrollbar } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import "./ha-domain-integrations"; +import "./ha-integration-list-item"; + +export interface IntegrationListItem { + name: string; + domain: string; + config_flow?: boolean; + is_helper?: boolean; + integrations?: string[]; + iot_standards?: string[]; + supported_flows?: string[]; + cloud?: boolean; + is_built_in?: boolean; + is_add?: boolean; +} + +@customElement("dialog-add-integration") +class AddIntegrationDialog extends LitElement { + public hass!: HomeAssistant; + + @state() private _integrations?: Integrations; + + @state() private _helpers?: Integrations; + + @state() private _supportedBrands?: Record; + + @state() private _initialFilter?: string; + + @state() private _filter?: string; + + @state() private _pickedBrand?: string; + + @state() private _flowsInProgress?: DataEntryFlowProgress[]; + + @state() private _open = false; + + @state() private _narrow = false; + + private _width?: number; + + private _height?: number; + + public showDialog(params): void { + this._open = true; + this._initialFilter = params.initialFilter; + this._narrow = matchMedia( + "all and (max-width: 450px), all and (max-height: 500px)" + ).matches; + } + + public closeDialog() { + this._open = false; + this._integrations = undefined; + this._helpers = undefined; + this._supportedBrands = undefined; + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + this._filter = undefined; + this._width = undefined; + this._height = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + public willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (this._filter === undefined && this._initialFilter !== undefined) { + this._filter = this._initialFilter; + } + if (this._initialFilter !== undefined && this._filter === "") { + this._initialFilter = undefined; + this._filter = ""; + this._width = undefined; + this._height = undefined; + } else if ( + this.hasUpdated && + changedProps.has("_filter") && + (!this._width || !this._height) + ) { + // Store the width and height so that when we search, box doesn't jump + const boundingRect = + this.shadowRoot!.querySelector("mwc-list")?.getBoundingClientRect(); + this._width = boundingRect?.width; + this._height = boundingRect?.height; + } + } + + public updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("_open") && this._open) { + this._load(); + } + } + + private _filterIntegrations = memoizeOne( + ( + i: Integrations, + h: Integrations, + sb: Record, + components: HomeAssistant["config"]["components"], + localize: LocalizeFunc, + filter?: string + ): IntegrationListItem[] => { + const addDeviceRows: IntegrationListItem[] = ["zha", "zwave_js"] + .filter((domain) => components.includes(domain)) + .map((domain) => ({ + name: localize(`ui.panel.config.integrations.add_${domain}_device`), + domain, + config_flow: true, + is_built_in: true, + is_add: true, + })) + .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)); + + const integrations: IntegrationListItem[] = Object.entries(i) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + iot_standards: integration.iot_standards, + integrations: integration.integrations + ? Object.entries(integration.integrations).map( + ([dom, val]) => val.name || domainToName(localize, dom) + ) + : undefined, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + + for (const [domain, domainBrands] of Object.entries(sb)) { + const integration = i[domain]; + if ( + !integration.config_flow && + !integration.iot_standards && + !integration.integrations + ) { + continue; + } + for (const [slug, name] of Object.entries(domainBrands)) { + integrations.push({ + domain: slug, + name, + config_flow: integration.config_flow, + supported_flows: [domain], + is_built_in: true, + cloud: integration.iot_class?.startsWith("cloud_"), + }); + } + } + + if (filter) { + const options: Fuse.IFuseOptions = { + keys: [ + "name", + "domain", + "supported_flows", + "integrations", + "iot_standards", + ], + isCaseSensitive: false, + minMatchCharLength: 2, + threshold: 0.2, + }; + const helpers = Object.entries(h) + .filter( + ([_domain, integration]) => + integration.config_flow || + integration.iot_standards || + integration.integrations + ) + .map(([domain, integration]) => ({ + domain, + name: integration.name || domainToName(localize, domain), + config_flow: integration.config_flow, + is_helper: true, + is_built_in: integration.is_built_in !== false, + cloud: integration.iot_class?.startsWith("cloud_"), + })); + return [ + ...new Fuse(integrations, options) + .search(filter) + .map((result) => result.item), + ...new Fuse(helpers, options) + .search(filter) + .map((result) => result.item), + ]; + } + return [ + ...addDeviceRows, + ...integrations.sort((a, b) => + caseInsensitiveStringCompare(a.name || "", b.name || "") + ), + ]; + } + ); + + private _getIntegrations() { + return this._filterIntegrations( + this._integrations!, + this._helpers!, + this._supportedBrands!, + this.hass.config.components, + this.hass.localize, + this._filter + ); + } + + protected render(): TemplateResult { + if (!this._open) { + return html``; + } + const integrations = this._integrations + ? this._getIntegrations() + : undefined; + + return html` + ${this._pickedBrand + ? html`
    + +

    + ${this._calculateBrandHeading()} +

    +
    + ${this._renderIntegration()}` + : this._renderAll(integrations)} +
    `; + } + + private _calculateBrandHeading() { + const brand = this._integrations?.[this._pickedBrand!]; + if ( + brand?.iot_standards && + !brand.integrations && + !this._flowsInProgress?.length + ) { + return "What type of device is it?"; + } + if ( + !brand?.iot_standards && + !brand?.integrations && + this._flowsInProgress?.length + ) { + return "Want to add these discovered devices?"; + } + return "What do you want to add?"; + } + + private _renderIntegration(): TemplateResult { + return html``; + } + + private _renderAll(integrations?: IntegrationListItem[]): TemplateResult { + return html` + ${integrations + ? html` + + + ` + : html``} `; + } + + private _renderRow = (integration: IntegrationListItem) => { + if (!integration) { + return html``; + } + return html` + + + `; + }; + + private async _load() { + const [descriptions, supportedBrands] = await Promise.all([ + getIntegrationDescriptions(this.hass), + getSupportedBrands(this.hass), + ]); + for (const integration in descriptions.custom.integration) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.integration, + integration + ) + ) { + continue; + } + descriptions.custom.integration[integration].is_built_in = false; + } + this._integrations = { + ...descriptions.core.integration, + ...descriptions.custom.integration, + }; + for (const integration in descriptions.custom.helper) { + if ( + !Object.prototype.hasOwnProperty.call( + descriptions.custom.helper, + integration + ) + ) { + continue; + } + descriptions.custom.helper[integration].is_built_in = false; + } + this._helpers = { + ...descriptions.core.helper, + ...descriptions.custom.helper, + }; + this._supportedBrands = supportedBrands; + this.hass.loadBackendTranslation( + "title", + descriptions.core.translated_name, + true + ); + } + + private async _filterChanged(e) { + this._filter = e.detail.value; + } + + private _integrationPicked(ev) { + const listItem = ev.target.closest("ha-integration-list-item"); + const integration: IntegrationListItem = listItem.integration; + this._handleIntegrationPicked(integration); + } + + private async _handleIntegrationPicked(integration: IntegrationListItem) { + if ("supported_flows" in integration) { + const domain = integration.supported_flows![0]; + + showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.supported_brand_flow", + { + supported_brand: integration.name, + flow_domain_name: domainToName(this.hass.localize, domain), + } + ), + confirm: () => { + const supportIntegration = this._integrations?.[domain]; + this.closeDialog(); + if (["zha", "zwave_js"].includes(domain)) { + protocolIntegrationPicked(this, this.hass, domain); + return; + } + if (supportIntegration) { + this._handleIntegrationPicked({ + domain, + name: + supportIntegration.name || + domainToName(this.hass.localize, domain), + config_flow: supportIntegration.config_flow, + iot_standards: supportIntegration.iot_standards, + integrations: supportIntegration.integrations + ? Object.entries(supportIntegration.integrations).map( + ([dom, val]) => + val.name || domainToName(this.hass.localize, dom) + ) + : undefined, + }); + } else { + showAlertDialog(this, { + text: "Integration not found", + warning: true, + }); + } + }, + }); + + return; + } + + if (integration.is_add) { + protocolIntegrationPicked(this, this.hass, integration.domain); + this.closeDialog(); + return; + } + + if (integration.is_helper) { + this.closeDialog(); + navigate(`/config/helpers/add?domain=${integration.domain}`); + return; + } + + if (integration.integrations) { + this._fetchFlowsInProgress(Object.keys(integration.integrations)); + this._pickedBrand = integration.domain; + return; + } + + if ( + ["zha", "zwave_js"].includes(integration.domain) && + isComponentLoaded(this.hass, integration.domain) + ) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.iot_standards) { + this._pickedBrand = integration.domain; + return; + } + + if (integration.config_flow) { + this._createFlow(integration); + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + this.closeDialog(); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_title" + ), + text: this.hass.localize( + "ui.panel.config.integrations.config_flow.yaml_only_text", + { + link: + manifest?.is_built_in || manifest?.documentation + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + )} + ` + : this.hass.localize( + "ui.panel.config.integrations.config_flow.documentation" + ), + } + ), + }); + } + + private async _createFlow(integration: IntegrationListItem) { + const flowsInProgress = await this._fetchFlowsInProgress([ + integration.domain, + ]); + + if (flowsInProgress?.length) { + this._pickedBrand = integration.domain; + return; + } + + const manifest = await fetchIntegrationManifest( + this.hass, + integration.domain + ); + + this.closeDialog(); + + showConfigFlowDialog(this, { + startFlowHandler: integration.domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest, + }); + } + + private async _fetchFlowsInProgress(domains: string[]) { + const flowsInProgress = ( + await fetchConfigFlowInProgress(this.hass.connection) + ).filter((flow) => domains.includes(flow.handler)); + + if (flowsInProgress.length) { + this._flowsInProgress = flowsInProgress; + } + return flowsInProgress; + } + + private _maybeSubmit(ev: KeyboardEvent) { + if (ev.key !== "Enter") { + return; + } + + const integrations = this._getIntegrations(); + + if (integrations.length > 0) { + this._handleIntegrationPicked(integrations[0]); + } + } + + private _prevClicked() { + this._pickedBrand = undefined; + this._flowsInProgress = undefined; + } + + static styles = [ + haStyleScrollbar, + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + } + search-input { + display: block; + margin: 16px 16px 0; + } + .divider { + border-bottom-color: var(--divider-color); + } + h2 { + padding-inline-end: 66px; + direction: var(--direction); + } + p { + text-align: center; + padding: 16px; + margin: 0; + } + p > a { + color: var(--primary-color); + } + ha-circular-progress { + width: 100%; + display: flex; + justify-content: center; + margin: 24px 0; + } + lit-virtualizer { + contain: size layout !important; + } + ha-integration-list-item { + width: 100%; + } + ha-icon-button-prev { + color: var(--secondary-text-color); + position: absolute; + left: 16px; + top: 14px; + inset-inline-end: initial; + inset-inline-start: 16px; + direction: var(--direction); + } + .mdc-dialog__title { + margin: 0; + margin-bottom: 8px; + margin-left: 48px; + padding: 24px 24px 0 24px; + color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87)); + font-size: var(--mdc-typography-headline6-font-size, 1.25rem); + line-height: var(--mdc-typography-headline6-line-height, 2rem); + font-weight: var(--mdc-typography-headline6-font-weight, 500); + letter-spacing: var( + --mdc-typography-headline6-letter-spacing, + 0.0125em + ); + text-decoration: var( + --mdc-typography-headline6-text-decoration, + inherit + ); + text-transform: var(--mdc-typography-headline6-text-transform, inherit); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-add-integration": AddIntegrationDialog; + } +} diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index daee2bee47..ac1caa367d 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -14,7 +14,6 @@ import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { fireEvent } from "../../../common/dom/fire_event"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; @@ -75,6 +74,7 @@ import "./ha-ignored-config-entry-card"; import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import "./ha-integration-overflow-menu"; +import { showAddIntegrationDialog } from "./show-add-integration-dialog"; export interface ConfigEntryUpdatedEvent { entry: ConfigEntry; @@ -312,7 +312,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { undefined, true ); - this._fetchManifests(); if (this.route.path === "/add") { this._handleAdd(localizePromise); } @@ -599,7 +598,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { // Make a copy so we can keep track of previously loaded manifests // for discovered flows (which are not part of these results) const manifests = { ...this._manifests }; - for (const manifest of fetched) manifests[manifest.domain] = manifest; + for (const manifest of fetched) { + manifests[manifest.domain] = manifest; + } this._manifests = manifests; } @@ -630,15 +631,9 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { } private _createFlow() { - showConfigFlowDialog(this, { - searchQuery: this._filter, - dialogClosedCallback: () => { - this._handleFlowUpdated(); - }, - showAdvanced: this.showAdvanced, + showAddIntegrationDialog(this, { + initialFilter: this._filter, }); - // For config entries. Also loading config flow ones for added integration - this.hass.loadBackendTranslation("title", undefined, true); } private _handleMenuAction(ev: CustomEvent) { @@ -735,9 +730,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { protocolIntegrationPicked(this, this.hass, slug); return; } - - fireEvent(this, "handler-picked", { - handler: slug, + showConfigFlowDialog(this, { + dialogClosedCallback: () => { + this._handleFlowUpdated(); + }, + startFlowHandler: slug, + manifest: this._manifests[slug], + showAdvanced: this.hass.userData?.showAdvanced, }); }, }); diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts new file mode 100644 index 0000000000..d54f9e2872 --- /dev/null +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -0,0 +1,225 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { localizeConfigFlowTitle } from "../../../data/config_flow"; +import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import { + domainToName, + fetchIntegrationManifest, +} from "../../../data/integration"; +import { Integration } from "../../../data/integrations"; +import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import "./ha-integration-list-item"; + +const standardToDomain = { zigbee: "zha", "z-wave": "zwave_js" } as const; + +@customElement("ha-domain-integrations") +class HaDomainIntegrations extends LitElement { + public hass!: HomeAssistant; + + @property() public domain!: string; + + @property({ attribute: false }) public integration!: Integration; + + @property({ attribute: false }) + public flowsInProgress?: DataEntryFlowProgress[]; + + protected render() { + return html` + ${this.flowsInProgress?.length + ? html`

    We discovered the following:

    + ${this.flowsInProgress.map( + (flow) => html` + + ${localizeConfigFlowTitle(this.hass.localize, flow)} + + ` + )}` + : ""} + ${this.integration?.iot_standards + ? this.integration.iot_standards.map((standard) => { + const domain: string = standardToDomain[standard] || standard; + return html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + )} + + `; + }) + : ""} + ${this.integration?.integrations + ? Object.entries(this.integration.integrations).map( + ([dom, val]) => html` + ` + ) + : ""} + ${["zha", "zwave_js"].includes(this.domain) + ? html` + + ${this.hass.localize( + `ui.panel.config.integrations.add_${this.domain}_device` + )} + + ` + : ""} + ${this.integration?.config_flow + ? html`${this.flowsInProgress?.length + ? html` + Setup another instance of + ${this.integration.name || + domainToName(this.hass.localize, this.domain)} + + ` + : html` + `}` + : ""} + `; + } + + private async _integrationPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + startFlowHandler: domain, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, domain), + } + ); + fireEvent(this, "close-dialog"); + } + + private async _flowInProgressPicked(ev) { + const flow: DataEntryFlowProgress = ev.currentTarget.flow; + const root = this.getRootNode(); + showConfigFlowDialog( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + { + continueFlowId: flow.flow_id, + showAdvanced: this.hass.userData?.showAdvanced, + manifest: await fetchIntegrationManifest(this.hass, flow.handler), + } + ); + fireEvent(this, "close-dialog"); + } + + private _standardPicked(ev) { + const domain = ev.currentTarget.domain; + const root = this.getRootNode(); + fireEvent(this, "close-dialog"); + protocolIntegrationPicked( + root instanceof ShadowRoot ? (root.host as HTMLElement) : this, + this.hass, + domain + ); + } + + static styles = [ + haStyle, + css` + :host { + display: block; + } + h3 { + margin: 0 24px; + color: var(--primary-text-color); + font-size: 14px; + } + img { + width: 40px; + height: 40px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-domain-integrations": HaDomainIntegrations; + } +} diff --git a/src/panels/config/integrations/ha-integration-list-item.ts b/src/panels/config/integrations/ha-integration-list-item.ts new file mode 100644 index 0000000000..26f28b5051 --- /dev/null +++ b/src/panels/config/integrations/ha-integration-list-item.ts @@ -0,0 +1,151 @@ +import { + GraphicType, + ListItemBase, +} from "@material/mwc-list/mwc-list-item-base"; +import { styles } from "@material/mwc-list/mwc-list-item.css"; +import { mdiCloudOutline, mdiCodeBraces, mdiPackageVariant } from "@mdi/js"; +import { css, CSSResultGroup, html } from "lit"; +import { classMap } from "lit/directives/class-map"; +import { customElement, property } from "lit/decorators"; +import { domainToName } from "../../../data/integration"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { IntegrationListItem } from "./dialog-add-integration"; + +@customElement("ha-integration-list-item") +export class HaIntegrationListItem extends ListItemBase { + public hass!: HomeAssistant; + + @property({ attribute: false }) public integration?: IntegrationListItem; + + @property({ type: String, reflect: true }) graphic: GraphicType = "medium"; + + @property({ type: Boolean }) hasMeta = true; + + renderSingleLine() { + if (!this.integration) { + return html``; + } + return html`${this.integration.name || + domainToName(this.hass.localize, this.integration.domain)} + ${this.integration.is_helper ? " (helper)" : ""}`; + } + + protected renderGraphic() { + if (!this.integration) { + return html``; + } + const graphicClasses = { + multi: this.multipleGraphics, + }; + + return html` + + `; + } + + protected renderMeta() { + if (!this.integration) { + return html``; + } + return html` + ${!this.integration.config_flow && + !this.integration.integrations && + !this.integration.iot_standards + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.yaml_only" + )}` + : ""} + ${this.integration.cloud + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.depends_on_cloud" + )}` + : ""} + ${!this.integration.is_built_in + ? html`${this.hass.localize( + "ui.panel.config.integrations.config_entry.provided_by_custom_integration" + )}` + : ""} + + `; + } + + static get styles(): CSSResultGroup { + return [ + styles, + css` + :host { + padding-left: var(--mdc-list-side-padding, 20px); + padding-right: var(--mdc-list-side-padding, 20px); + } + :host([graphic="avatar"]:not([twoLine])), + :host([graphic="icon"]:not([twoLine])) { + height: 48px; + } + span.material-icons:first-of-type { + margin-inline-start: 0px !important; + margin-inline-end: var( + --mdc-list-item-graphic-margin, + 16px + ) !important; + direction: var(--direction); + } + span.material-icons:last-of-type { + margin-inline-start: auto !important; + margin-inline-end: 0px !important; + direction: var(--direction); + } + img { + width: 40px; + height: 40px; + } + .mdc-deprecated-list-item__meta { + width: auto; + } + .mdc-deprecated-list-item__meta > * { + margin-right: 8px; + } + .mdc-deprecated-list-item__meta > *:last-child { + margin-right: 0px; + } + ha-icon-next { + margin-right: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-integration-list-item": HaIntegrationListItem; + } +} diff --git a/src/panels/config/integrations/show-add-integration-dialog.ts b/src/panels/config/integrations/show-add-integration-dialog.ts new file mode 100644 index 0000000000..cb3fb8b9e1 --- /dev/null +++ b/src/panels/config/integrations/show-add-integration-dialog.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const showAddIntegrationDialog = ( + element: HTMLElement, + dialogParams?: any +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-add-integration", + dialogImport: () => import("./dialog-add-integration"), + dialogParams: dialogParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 5bbe3ca627..139fdd1707 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2835,7 +2835,7 @@ "discovered": "Discovered", "attention": "Attention required", "configured": "Configured", - "new": "Set up a new integration", + "new": "Select brand", "confirm_new": "Do you want to set up {integration}?", "add_integration": "Add integration", "no_integrations": "Seems like you don't have any integrations configured yet. Click on the button below to add your first integration!", @@ -2852,6 +2852,7 @@ "rename_dialog": "Edit the name of this config entry", "rename_input_label": "Entry name", "search": "Search integrations", + "search_brand": "Search for a brand name", "add_zwave_js_device": "Add Z-Wave device", "add_zha_device": "Add Zigbee device", "disable": { @@ -2922,6 +2923,7 @@ }, "provided_by_custom_integration": "Provided by a custom integration", "depends_on_cloud": "Depends on the cloud", + "yaml_only": "Can not be setup from the UI", "disabled_polling": "Automatic polling for updated data disabled", "state": { "loaded": "Loaded", @@ -2943,6 +2945,9 @@ "submit": "Submit", "next": "Next", "found_following_devices": "We found the following devices", + "yaml_only_title": "This device can not be added from the UI", + "yaml_only_text": "You can add this device by adding it to your `configuration.yaml`. See the {link} for more information.", + "documentation": "documentation", "no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.", "not_all_required_fields": "Not all required fields are filled in.", "error_saving_area": "Error saving area: {error}", @@ -2963,6 +2968,7 @@ "error": "Error", "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", + "missing_credentials_title": "Add application credentials?", "missing_credentials": "Setting up {integration} requires configuring application credentials. Do you want to do that now?", "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", "missing_zwave_zigbee": "To add a {integration} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.", From 182b8f809c6bbe236f3107b1a59d4a00b2845597 Mon Sep 17 00:00:00 2001 From: Mark Parker <11211952+msp1974@users.noreply.github.com> Date: Wed, 28 Sep 2022 16:40:08 +0100 Subject: [PATCH 96/98] Prevent service call error if preset_mode is null (#13825) --- .../more-info/controls/more-info-climate.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 2a66080055..03b94c3827 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -377,12 +377,14 @@ class MoreInfoClimate extends LitElement { private _handlePresetmodeChanged(ev) { const newVal = ev.target.value || null; - this._callServiceHelper( - this.stateObj!.attributes.preset_mode, - newVal, - "set_preset_mode", - { preset_mode: newVal } - ); + if (newVal) { + this._callServiceHelper( + this.stateObj!.attributes.preset_mode, + newVal, + "set_preset_mode", + { preset_mode: newVal } + ); + } } private async _callServiceHelper( From 594ee85bbe7c6c7e08f995b93b7d1d737a6bc042 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 28 Sep 2022 08:41:32 -0700 Subject: [PATCH 97/98] Add stream orientation to camera entity registry settings (#13512) --- src/data/camera.ts | 4 ++ .../entities/entity-registry-settings.ts | 56 ++++++++++++++++++- src/translations/en.json | 16 +++++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/data/camera.ts b/src/data/camera.ts index cca3671377..26d928f754 100644 --- a/src/data/camera.ts +++ b/src/data/camera.ts @@ -6,6 +6,7 @@ import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-pro import { HomeAssistant } from "../types"; import { getSignedPath } from "./auth"; +export const CAMERA_ORIENTATIONS = [1, 2, 3, 4, 6, 8]; export const CAMERA_SUPPORT_ON_OFF = 1; export const CAMERA_SUPPORT_STREAM = 2; @@ -26,6 +27,7 @@ export interface CameraEntity extends HassEntityBase { export interface CameraPreferences { preload_stream: boolean; + orientation: number; } export interface CameraThumbnail { @@ -109,11 +111,13 @@ export const fetchCameraPrefs = (hass: HomeAssistant, entityId: string) => entity_id: entityId, }); +type ValueOf = T[number]; export const updateCameraPrefs = ( hass: HomeAssistant, entityId: string, prefs: { preload_stream?: boolean; + orientation?: ValueOf; } ) => hass.callWS({ diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts index f554e5bbf4..e87fd4ea02 100644 --- a/src/panels/config/entities/entity-registry-settings.ts +++ b/src/panels/config/entities/entity-registry-settings.ts @@ -19,7 +19,10 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { domainIcon } from "../../../common/entity/domain_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { stringCompare } from "../../../common/string/compare"; -import { LocalizeFunc } from "../../../common/translations/localize"; +import { + LocalizeFunc, + LocalizeKeys, +} from "../../../common/translations/localize"; import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; import "../../../components/ha-expansion-panel"; @@ -32,6 +35,7 @@ import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import { CameraPreferences, + CAMERA_ORIENTATIONS, CAMERA_SUPPORT_STREAM, fetchCameraPrefs, STREAM_TYPE_HLS, @@ -586,12 +590,12 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) { ${this.hass.localize( - "ui.dialogs.entity_registry.editor.preload_stream" + "ui.dialogs.entity_registry.editor.stream.preload_stream" )} ${this.hass.localize( - "ui.dialogs.entity_registry.editor.preload_stream_description" + "ui.dialogs.entity_registry.editor.stream.preload_stream_description" )} + + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation" + )} + ${this.hass.localize( + "ui.dialogs.entity_registry.editor.stream.stream_orientation_description" + )} + + ${CAMERA_ORIENTATIONS.map((num) => { + const localizeStr = + "ui.dialogs.entity_registry.editor.stream.stream_orientation_" + + num.toString(); + return html` + + ${this.hass.localize(localizeStr as LocalizeKeys)} + + `; + })} + + ` : ""} Date: Wed, 28 Sep 2022 17:43:09 +0200 Subject: [PATCH 98/98] Bumped version to 20220928.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 17f53754f1..392614fdd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20220907.0" +version = "20220928.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"