diff --git a/gallery/src/pages/automation/describe-condition.ts b/gallery/src/pages/automation/describe-condition.ts index 2f6cef5f47..3350dce74f 100644 --- a/gallery/src/pages/automation/describe-condition.ts +++ b/gallery/src/pages/automation/describe-condition.ts @@ -36,6 +36,7 @@ const conditions = [ { condition: "sun", after: "sunset" }, { condition: "sun", after: "sunrise", offset: "-01:00" }, { condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" }, + { condition: "trigger", id: "motion" }, { condition: "time" }, { condition: "template" }, ]; diff --git a/pyproject.toml b/pyproject.toml index cf6bcf6ef9..49715f19bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20220929.0" +version = "20221002.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index ea566c9fc7..8a597fd56a 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -21,6 +21,7 @@ import { numberFormatToLocale, } from "../../common/number/format_number"; import { + getDisplayUnit, getStatisticLabel, getStatisticMetadata, Statistics, @@ -258,8 +259,11 @@ class StatisticsChart extends LitElement { if (!this.unit) { if (unit === undefined) { - unit = meta?.display_unit_of_measurement; - } else if (unit !== meta?.display_unit_of_measurement) { + unit = getDisplayUnit(this.hass, firstStat.statistic_id, meta); + } else if ( + unit !== getDisplayUnit(this.hass, firstStat.statistic_id, meta) + ) { + // Clear unit if not all statistics have same unit unit = null; } } diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index ebf129efaa..0b710f68ee 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -5,9 +5,12 @@ 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"; -import { getStatisticIds, StatisticsMetaData } from "../../data/recorder"; +import { + getStatisticIds, + getStatisticLabel, + StatisticsMetaData, +} from "../../data/recorder"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; @@ -43,18 +46,11 @@ export class HaStatisticPicker extends LitElement { public includeStatisticsUnitOfMeasurement?: string | string[]; /** - * Show only statistics displayed with these units of measurements. - * @attr include-display-unit-of-measurement + * Show only statistics with these unit classes. + * @attr include-unit-class */ - @property({ attribute: "include-display-unit-of-measurement" }) - public includeDisplayUnitOfMeasurement?: string | string[]; - - /** - * Show only statistics with these device classes. - * @attr include-device-classes - */ - @property({ attribute: "include-device-classes" }) - public includeDeviceClasses?: string[]; + @property({ attribute: "include-unit-class" }) + public includeUnitClass?: string | string[]; /** * Show only statistics on entities. @@ -97,8 +93,7 @@ export class HaStatisticPicker extends LitElement { ( statisticIds: StatisticsMetaData[], includeStatisticsUnitOfMeasurement?: string | string[], - includeDisplayUnitOfMeasurement?: string | string[], - includeDeviceClasses?: string[], + includeUnitClass?: string | string[], entitiesOnly?: boolean ): Array<{ id: string; name: string; state?: HassEntity }> => { if (!statisticIds.length) { @@ -113,15 +108,18 @@ export class HaStatisticPicker extends LitElement { } if (includeStatisticsUnitOfMeasurement) { - const includeUnits = ensureArray(includeStatisticsUnitOfMeasurement); + const includeUnits: (string | null)[] = ensureArray( + includeStatisticsUnitOfMeasurement + ); statisticIds = statisticIds.filter((meta) => includeUnits.includes(meta.statistics_unit_of_measurement) ); } - if (includeDisplayUnitOfMeasurement) { - const includeUnits = ensureArray(includeDisplayUnitOfMeasurement); + if (includeUnitClass) { + const includeUnitClasses: (string | null)[] = + ensureArray(includeUnitClass); statisticIds = statisticIds.filter((meta) => - includeUnits.includes(meta.display_unit_of_measurement) + includeUnitClasses.includes(meta.unit_class) ); } @@ -136,23 +134,16 @@ export class HaStatisticPicker extends LitElement { if (!entitiesOnly) { output.push({ id: meta.statistic_id, - name: meta.name || meta.statistic_id, + name: getStatisticLabel(this.hass, meta.statistic_id, meta), }); } return; } - if ( - !includeDeviceClasses || - includeDeviceClasses.includes( - entityState!.attributes.device_class || "" - ) - ) { - output.push({ - id: meta.statistic_id, - name: computeStateName(entityState), - state: entityState, - }); - } + output.push({ + id: meta.statistic_id, + name: getStatisticLabel(this.hass, meta.statistic_id, meta), + state: entityState, + }); }); if (!output.length) { @@ -203,8 +194,7 @@ export class HaStatisticPicker extends LitElement { (this.comboBox as any).items = this._getStatistics( this.statisticIds!, this.includeStatisticsUnitOfMeasurement, - this.includeDisplayUnitOfMeasurement, - this.includeDeviceClasses, + this.includeUnitClass, this.entitiesOnly ); } else { @@ -212,8 +202,7 @@ export class HaStatisticPicker extends LitElement { (this.comboBox as any).items = this._getStatistics( this.statisticIds!, this.includeStatisticsUnitOfMeasurement, - this.includeDisplayUnitOfMeasurement, - this.includeDeviceClasses, + this.includeUnitClass, this.entitiesOnly ); }); diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index ef450d4ae2..62d7c0d2b7 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -32,11 +32,11 @@ class HaStatisticsPicker extends LitElement { public includeStatisticsUnitOfMeasurement?: string[] | string; /** - * Show only statistics displayed with these units of measurements. - * @attr include-display-unit-of-measurement + * Show only statistics with these unit classes. + * @attr include-unit-class */ - @property({ attribute: "include-display-unit-of-measurement" }) - public includeDisplayUnitOfMeasurement?: string[] | string; + @property({ attribute: "include-unit-class" }) + public includeUnitClass?: string | string[]; /** * Ignore filtering of statistics type and units when only a single statistic is selected. @@ -58,12 +58,12 @@ class HaStatisticsPicker extends LitElement { this.ignoreRestrictionsOnFirstStatistic && this._currentStatistics.length <= 1; - const includeDisplayUnitCurrent = ignoreRestriction - ? undefined - : this.includeDisplayUnitOfMeasurement; const includeStatisticsUnitCurrent = ignoreRestriction ? undefined : this.includeStatisticsUnitOfMeasurement; + const includeUnitClassCurrent = ignoreRestriction + ? undefined + : this.includeUnitClass; const includeStatisticTypesCurrent = ignoreRestriction ? undefined : this.statisticTypes; @@ -75,8 +75,8 @@ class HaStatisticsPicker extends LitElement { ( "vaadin-combo-box-overlay" ); @@ -268,6 +277,16 @@ export class HaComboBox extends LitElement { } } + updated(changedProps: PropertyValues) { + super.updated(changedProps); + if ( + changedProps.has("filteredItems") || + (changedProps.has("items") && this.opened) + ) { + this.removeInertOnOverlay(); + } + } + private _filterChanged(ev: ComboBoxLightFilterChangedEvent) { // @ts-ignore fireEvent(this, ev.type, ev.detail, { composed: false }); diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index 3f43d497e0..a424ffeda1 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -13,7 +13,7 @@ type IconItem = { icon: string; keywords: string[]; }; -let iconItems: IconItem[] = [{ icon: "", keywords: [] }]; +let iconItems: IconItem[] = []; let iconLoaded = false; // eslint-disable-next-line lit/prefer-static-styles diff --git a/src/components/ha-navigation-picker.ts b/src/components/ha-navigation-picker.ts index 84c1084b30..a9226d1e5e 100644 --- a/src/components/ha-navigation-picker.ts +++ b/src/components/ha-navigation-picker.ts @@ -20,7 +20,7 @@ type NavigationItem = { title: string; }; -const DEFAULT_ITEMS: NavigationItem[] = [{ path: "", icon: "", title: "" }]; +const DEFAULT_ITEMS: NavigationItem[] = []; // eslint-disable-next-line lit/prefer-static-styles const rowRenderer: ComboBoxLitRenderer = (item) => html` diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 8b17c894ed..c4a49721fe 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -189,7 +189,11 @@ export const describeTrigger = ( // Time Trigger if (trigger.platform === "time" && trigger.at) { const at = trigger.at.includes(".") - ? `entity ${computeStateName(hass.states[trigger.at]) || trigger.at}` + ? `entity ${ + hass.states[trigger.at] + ? computeStateName(hass.states[trigger.at]) + : trigger.at + }` : trigger.at; return `When the time is equal to ${at}`; @@ -568,6 +572,13 @@ export const describeCondition = ( }`; } + if (condition.condition === "trigger") { + if (!condition.id) { + return "Trigger condition"; + } + return `When triggered by ${condition.id}`; + } + return `${ condition.condition ? condition.condition.replace(/_/g, " ") : "Unknown" } condition`; diff --git a/src/data/energy.ts b/src/data/energy.ts index 8420419978..80dcfbb511 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -600,20 +600,14 @@ export const getEnergySolarForecasts = (hass: HomeAssistant) => type: "energy/solar_forecast", }); -export const ENERGY_GAS_VOLUME_UNITS = ["m³"]; -export const ENERGY_GAS_ENERGY_UNITS = ["kWh"]; -export const ENERGY_GAS_UNITS = [ - ...ENERGY_GAS_VOLUME_UNITS, - ...ENERGY_GAS_ENERGY_UNITS, -]; +const energyGasUnitClass = ["volume", "energy"] as const; +export type EnergyGasUnitClass = typeof energyGasUnitClass[number]; -export type EnergyGasUnit = "volume" | "energy"; - -export const getEnergyGasUnitCategory = ( +export const getEnergyGasUnitClass = ( prefs: EnergyPreferences, statisticsMetaData: Record = {}, excludeSource?: string -): EnergyGasUnit | undefined => { +): EnergyGasUnitClass | undefined => { for (const source of prefs.energy_sources) { if (source.type !== "gas") { continue; @@ -622,29 +616,29 @@ export const getEnergyGasUnitCategory = ( continue; } const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; - if (statisticIdWithMeta) { - return ENERGY_GAS_VOLUME_UNITS.includes( - statisticIdWithMeta.statistics_unit_of_measurement + if ( + energyGasUnitClass.includes( + statisticIdWithMeta.unit_class as EnergyGasUnitClass ) - ? "volume" - : "energy"; + ) { + return statisticIdWithMeta.unit_class as EnergyGasUnitClass; } } return undefined; }; export const getEnergyGasUnit = ( + hass: HomeAssistant, prefs: EnergyPreferences, statisticsMetaData: Record = {} ): string | undefined => { - for (const source of prefs.energy_sources) { - if (source.type !== "gas") { - continue; - } - const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; - if (statisticIdWithMeta?.display_unit_of_measurement) { - return statisticIdWithMeta.display_unit_of_measurement; - } + const unitClass = getEnergyGasUnitClass(prefs, statisticsMetaData); + if (unitClass === undefined) { + return undefined; } - return undefined; + return unitClass === "energy" + ? "kWh" + : hass.config.unit_system.length === "km" + ? "m³" + : "ft³"; }; diff --git a/src/data/recorder.ts b/src/data/recorder.ts index c13e8bc210..5d6fd71806 100644 --- a/src/data/recorder.ts +++ b/src/data/recorder.ts @@ -20,13 +20,13 @@ export interface StatisticValue { } export interface StatisticsMetaData { - display_unit_of_measurement: string; - statistics_unit_of_measurement: string; + statistics_unit_of_measurement: string | null; statistic_id: string; source: string; name?: string | null; has_sum: boolean; has_mean: boolean; + unit_class: string | null; } export type StatisticsValidationResult = @@ -254,14 +254,14 @@ export const adjustStatisticsSum = ( statistic_id: string, start_time: string, adjustment: number, - display_unit: string + adjustment_unit_of_measurement: string | null ): Promise => hass.callWS({ type: "recorder/adjust_sum_statistics", statistic_id, start_time, adjustment, - display_unit, + adjustment_unit_of_measurement, }); export const getStatisticLabel = ( @@ -275,3 +275,17 @@ export const getStatisticLabel = ( } return statisticsMetaData?.name || statisticsId; }; + +export const getDisplayUnit = ( + hass: HomeAssistant, + statisticsId: string | undefined, + statisticsMetaData: StatisticsMetaData | undefined +): string | null | undefined => { + let unit: string | undefined; + if (statisticsId) { + unit = hass.states[statisticsId]?.attributes.unit_of_measurement; + } + return unit === undefined + ? statisticsMetaData?.statistics_unit_of_measurement + : unit; +}; diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index 08b8a46df1..818c48b94f 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -14,8 +14,6 @@ import { showAddApplicationCredentialDialog } from "../../panels/config/applicat import { configFlowContentStyles } from "./styles"; 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 { @@ -54,26 +52,11 @@ 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) { - return; - } // Prompt to enter credentials and restart integration setup showAddApplicationCredentialDialog(this.params.dialogParentElement!, { selectedDomain: this.domain, + manifest: this.params.manifest, applicationCredentialAddedCallback: () => { showConfigFlowDialog(this.params.dialogParentElement!, { dialogClosedCallback: this.params.dialogClosedCallback, 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 f1fc6ecf87..f6d017a9ee 100644 --- a/src/panels/config/application_credentials/dialog-add-application-credential.ts +++ b/src/panels/config/application_credentials/dialog-add-application-credential.ts @@ -5,6 +5,7 @@ import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; 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 "../../../components/ha-circular-progress"; import "../../../components/ha-combo-box"; import { createCloseHeading } from "../../../components/ha-dialog"; @@ -16,7 +17,7 @@ import { createApplicationCredential, fetchApplicationCredentialsConfig, } from "../../../data/application_credential"; -import { domainToName } from "../../../data/integration"; +import { domainToName, IntegrationManifest } from "../../../data/integration"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; @@ -44,6 +45,8 @@ export class DialogAddApplicationCredential extends LitElement { @state() private _domain?: string; + @state() private _manifest?: IntegrationManifest | null; + @state() private _name?: string; @state() private _description?: string; @@ -58,8 +61,8 @@ export class DialogAddApplicationCredential extends LitElement { public showDialog(params: AddApplicationCredentialDialogParams) { this._params = params; - this._domain = - params.selectedDomain !== undefined ? params.selectedDomain : ""; + this._domain = params.selectedDomain; + this._manifest = params.manifest; this._name = ""; this._description = ""; this._clientId = ""; @@ -76,7 +79,7 @@ export class DialogAddApplicationCredential extends LitElement { name: domainToName(this.hass.localize, domain), })); await this.hass.loadBackendTranslation("application_credentials"); - if (this._domain !== "") { + if (this._domain) { this._updateDescription(); } } @@ -85,6 +88,9 @@ export class DialogAddApplicationCredential extends LitElement { if (!this._params || !this._domains) { return html``; } + const selectedDomainName = this._params.selectedDomain + ? domainToName(this.hass.localize, this._domain!) + : ""; return html`
- ${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._error + ? html`${this._error} ` + : ""} + ${this._params.selectedDomain && !this._description + ? html`

+ ${this.hass.localize( + "ui.panel.config.application_credentials.editor.missing_credentials", + { + integration: selectedDomainName, + } + )} + ${this._manifest?.is_built_in || this._manifest?.documentation + ? html` + ${this.hass.localize( + "ui.panel.config.application_credentials.editor.missing_credentials_domain_link", + { + integration: selectedDomainName, + } + )} + + ` + : ""} +

` + : ""} + ${!this._params.selectedDomain || !this._description + ? html`

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

` + : ""} + ${this._params.selectedDomain + ? "" + : html``} ${this._description ? html` void; dialogAbortedCallback?: () => void; selectedDomain?: string; + manifest?: IntegrationManifest | null; } export const loadAddApplicationCredentialDialog = () => 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 b256e6ba50..9158f61106 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 @@ -55,7 +55,7 @@ export class HaChooseAction extends LitElement implements ActionElement { )}: (option.conditions)} .reOrderMode=${this.reOrderMode} .disabled=${this.disabled} .hass=${this.hass} @@ -68,7 +68,7 @@ export class HaChooseAction extends LitElement implements ActionElement { )}: { - this._config = c.config; + this._config = this._normalizeConfig(c.config); }); this._entityId = this.entityId; this._dirty = false; @@ -473,24 +473,27 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { this._entityId = automation?.entity_id; } + private _normalizeConfig(config: AutomationConfig): AutomationConfig { + // Normalize data: ensure trigger, action and condition are lists + // Happens when people copy paste their automations into the config + for (const key of ["trigger", "condition", "action"]) { + const value = config[key]; + if (value && !Array.isArray(value)) { + config[key] = [value]; + } + } + return config; + } + private async _loadConfig() { try { const config = await fetchAutomationFileConfig( this.hass, this.automationId as string ); - - // Normalize data: ensure trigger, action and condition are lists - // Happens when people copy paste their automations into the config - for (const key of ["trigger", "condition", "action"]) { - const value = config[key]; - if (value && !Array.isArray(value)) { - config[key] = [value]; - } - } this._dirty = false; this._readOnly = false; - this._config = config; + this._config = this._normalizeConfig(config); } catch (err: any) { const entity = Object.values(this.hass.entities).find( (ent) => diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 3d0327ff74..fbe82913e9 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -35,6 +35,8 @@ import { deleteAutomation, duplicateAutomation, fetchAutomationFileConfig, + getAutomationStateConfig, + showAutomationEditor, triggerAutomationActions, } from "../../../data/automation"; import { @@ -329,6 +331,14 @@ class HaAutomationPicker extends LitElement { } private _showTrace(automation: any) { + if (!automation.attributes.id) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.panel.config.automation.picker.traces_not_available" + ), + }); + return; + } navigate(`/config/automation/trace/${automation.attributes.id}`); } @@ -382,17 +392,20 @@ class HaAutomationPicker extends LitElement { ); duplicateAutomation(config); } catch (err: any) { + if (err.status_code === 404) { + const response = await getAutomationStateConfig( + this.hass, + automation.entity_id + ); + showAutomationEditor({ ...response.config, id: undefined }); + return; + } await showAlertDialog(this, { - text: - err.status_code === 404 - ? this.hass.localize( - "ui.panel.config.automation.editor.load_error_not_duplicable" - ) - : this.hass.localize( - "ui.panel.config.automation.editor.load_error_unknown", - "err_no", - err.status_code - ), + text: this.hass.localize( + "ui.panel.config.automation.editor.load_error_unknown", + "err_no", + err.status_code + ), }); } } diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 4ef701aba1..078ec02f79 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -38,6 +38,7 @@ import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; +import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; interface DeviceRowData extends DeviceRegistryEntry { device?: DeviceRowData; @@ -363,16 +364,15 @@ export class HaConfigDeviceDashboard extends LitElement { } protected render(): TemplateResult { - const { devicesOutput, filteredConfigEntry } = - this._devicesAndFilterDomains( - this.devices, - this.entries, - this.entities, - this.areas, - this._searchParms, - this._showDisabled, - this.hass.localize - ); + const { devicesOutput } = this._devicesAndFilterDomains( + this.devices, + this.entries, + this.entities, + this.areas, + this._searchParms, + this._showDisabled, + this.hass.localize + ); const activeFilters = this._activeFilters( this.entries, this._searchParms, @@ -405,39 +405,21 @@ export class HaConfigDeviceDashboard extends LitElement { @search-changed=${this._handleSearchChange} @row-click=${this._handleRowClicked} clickable - .hasFab=${filteredConfigEntry && - (filteredConfigEntry.domain === "zha" || - filteredConfigEntry.domain === "zwave_js")} + hasFab > - ${!filteredConfigEntry - ? "" - : filteredConfigEntry.domain === "zwave_js" - ? html` - - - - ` - : filteredConfigEntry.domain === "zha" - ? html` - - - - ` - : html``} + + + ) { if (ev.detail.value) { - const entity = this.hass.states[ev.detail.value]; - if (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; - } + const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]); + this._pickedDisplayUnit = getDisplayUnit( + this.hass, + ev.detail.value, + metadata[0] + ); } else { this._pickedDisplayUnit = undefined; } 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 c2cab0d7b2..68e9879bf8 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 @@ -20,8 +20,7 @@ import "../../../../components/ha-formfield"; import type { HaRadio } from "../../../../components/ha-radio"; import "../../../../components/entity/ha-entity-picker"; -const energyUnits = ["kWh"]; -const energyDeviceClasses = ["energy"]; +const energyUnitClasses = ["energy"]; @customElement("dialog-energy-grid-flow-settings") export class DialogEnergyGridFlowSettings @@ -93,8 +92,7 @@ export class DialogEnergyGridFlowSettings Promise; } diff --git a/src/panels/config/helpers/forms/ha-schedule-form.ts b/src/panels/config/helpers/forms/ha-schedule-form.ts index c28e90f8f0..7aa682ae5c 100644 --- a/src/panels/config/helpers/forms/ha-schedule-form.ts +++ b/src/panels/config/helpers/forms/ha-schedule-form.ts @@ -219,19 +219,18 @@ class HaScheduleForm extends LitElement { const start = new Date(); start.setDate(start.getDate() + distance); + const start_tokens = item.from.split(":"); start.setHours( - parseInt(item.from.slice(0, 2)), - parseInt(item.from.slice(-2)) + parseInt(start_tokens[0]), + parseInt(start_tokens[1]), + 0, + 0 ); const end = new Date(); end.setDate(end.getDate() + distance); - end.setHours( - parseInt(item.to.slice(0, 2)), - parseInt(item.to.slice(-2)), - 0, - 0 - ); + const end_tokens = item.to.split(":"); + end.setHours(parseInt(end_tokens[0]), parseInt(end_tokens[1]), 0, 0); events.push({ id: `${day}-${index}`, diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index 8df3b20903..34aea54d2b 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -80,10 +80,10 @@ class AddIntegrationDialog extends LitElement { private _height?: number; - public showDialog(params: AddIntegrationDialogParams): void { + public showDialog(params?: AddIntegrationDialogParams): void { this._open = true; - this._pickedBrand = params.brand; - this._initialFilter = params.initialFilter; + this._pickedBrand = params?.brand; + this._initialFilter = params?.initialFilter; this._narrow = matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" ).matches; @@ -296,14 +296,13 @@ class AddIntegrationDialog extends LitElement { scrimClickAction escapeKeyAction hideActions - .heading=${this._pickedBrand - ? true - : createCloseHeading( - this.hass, - this.hass.localize("ui.panel.config.integrations.new") - )} + .heading=${createCloseHeading( + this.hass, + this.hass.localize("ui.panel.config.integrations.new") + )} > - ${this._pickedBrand + ${this._pickedBrand && + (!this._integrations || this._pickedBrand in this._integrations) ? html`
+ `; }; @@ -506,7 +509,16 @@ class AddIntegrationDialog extends LitElement { if (integration.integrations) { const integrations = this._integrations![integration.domain].integrations!; - this._fetchFlowsInProgress(Object.keys(integrations)); + let domains = Object.keys(integrations); + if (integration.iot_standards?.includes("homekit")) { + // if homekit is supported, also fetch the discovered homekit devices + domains.push("homekit_controller"); + } + if (integration.domain === "apple") { + // we show discoverd homekit devices in their own brand section, dont show them at apple + domains = domains.filter((domain) => domain !== "homekit_controller"); + } + this._fetchFlowsInProgress(domains); this._pickedBrand = integration.domain; return; } @@ -529,6 +541,15 @@ class AddIntegrationDialog extends LitElement { return; } + if ( + ["cloud", "google_assistant", "alexa"].includes(integration.domain) && + isComponentLoaded(this.hass, "cloud") + ) { + this.closeDialog(); + navigate("/config/cloud"); + return; + } + const manifest = await fetchIntegrationManifest( this.hass, integration.domain @@ -591,7 +612,14 @@ class AddIntegrationDialog extends LitElement { private async _fetchFlowsInProgress(domains: string[]) { const flowsInProgress = ( await fetchConfigFlowInProgress(this.hass.connection) - ).filter((flow) => domains.includes(flow.handler)); + ).filter( + (flow) => + // filter config flows that are not for the integration we are looking for + domains.includes(flow.handler) || + // filter config flows of other domains (like homekit) that are for the domains we are looking for + ("alternative_domain" in flow.context && + domains.includes(flow.context.alternative_domain)) + ); if (flowsInProgress.length) { this._flowsInProgress = flowsInProgress; diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index c4b739206a..610414d77f 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -1,7 +1,9 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +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 { localizeConfigFlowTitle } from "../../../data/config_flow"; import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; @@ -26,7 +28,7 @@ class HaDomainIntegrations extends LitElement { @property() public domain!: string; - @property({ attribute: false }) public integration!: Integration; + @property({ attribute: false }) public integration?: Integration; @property({ attribute: false }) public flowsInProgress?: DataEntryFlowProgress[]; @@ -179,9 +181,22 @@ class HaDomainIntegrations extends LitElement { private async _integrationPicked(ev) { const domain = ev.currentTarget.domain; + if ( - (domain === this.domain && !this.integration.config_flow) || - !this.integration.integrations?.[domain]?.config_flow + ["cloud", "google_assistant", "alexa"].includes(domain) && + isComponentLoaded(this.hass, "cloud") + ) { + fireEvent(this, "close-dialog"); + navigate("/config/cloud"); + return; + } + + if ( + (domain === this.domain && + !this.integration!.config_flow && + (!this.integration!.integrations?.[domain] || + !this.integration!.integrations[domain].config_flow)) || + !this.integration!.integrations?.[domain]?.config_flow ) { const manifest = await fetchIntegrationManifest(this.hass, domain); showAlertDialog(this, { diff --git a/src/panels/config/integrations/ha-integration-list-item.ts b/src/panels/config/integrations/ha-integration-list-item.ts index 714f15d667..b117d131a9 100644 --- a/src/panels/config/integrations/ha-integration-list-item.ts +++ b/src/panels/config/integrations/ha-integration-list-item.ts @@ -22,6 +22,8 @@ export class HaIntegrationListItem extends ListItemBase { @property({ type: Boolean }) hasMeta = true; + @property({ type: Boolean }) brand = false; + renderSingleLine() { if (!this.integration) { return html``; @@ -51,6 +53,7 @@ export class HaIntegrationListItem extends ListItemBase { type: "icon", useFallback: true, darkOptimized: this.hass.themes?.darkMode, + brand: this.brand, })} referrerpolicy="no-referrer" /> diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index dd81e9dd8f..e842a9c949 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -469,15 +469,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ) { fetchScriptFileConfig(this.hass, this.scriptId).then( (config) => { - // Normalize data: ensure sequence is a list - // Happens when people copy paste their scripts into the config - const value = config.sequence; - if (value && !Array.isArray(value)) { - config.sequence = [value]; - } this._dirty = false; this._readOnly = false; - this._config = config; + this._config = this._normalizeConfig(config); }, (resp) => { const entity = Object.values(this.hass.entities).find( @@ -524,7 +518,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { if (changedProps.has("entityId") && this.entityId) { getScriptStateConfig(this.hass, this.entityId).then((c) => { - this._config = c.config; + this._config = this._normalizeConfig(c.config); }); const regEntry = this.hass.entities[this.entityId]; if (regEntry?.unique_id) { @@ -536,6 +530,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } + private _normalizeConfig(config: ScriptConfig): ScriptConfig { + // Normalize data: ensure sequence is a list + // Happens when people copy paste their scripts into the config + const value = config.sequence; + if (value && !Array.isArray(value)) { + config.sequence = [value]; + } + return config; + } + private _computeLabelCallback = ( schema: SchemaUnion>, data: HaFormDataContainer diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index c9e8dbfd85..a4f4891ae3 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -28,6 +28,7 @@ import "../../../components/ha-svg-icon"; import { deleteScript, fetchScriptFileConfig, + getScriptStateConfig, showScriptEditor, triggerScript, } from "../../../data/script"; @@ -311,17 +312,20 @@ class HaScriptPicker extends LitElement { )})`, }); } catch (err: any) { + if (err.status_code === 404) { + const response = await getScriptStateConfig( + this.hass, + script.entity_id + ); + showScriptEditor(response.config); + return; + } await showAlertDialog(this, { - text: - err.status_code === 404 - ? this.hass.localize( - "ui.panel.config.script.editor.load_error_not_duplicable" - ) - : this.hass.localize( - "ui.panel.config.script.editor.load_error_unknown", - "err_no", - err.status_code - ), + text: this.hass.localize( + "ui.panel.config.script.editor.load_error_unknown", + "err_no", + err.status_code + ), }); } } diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index 40a55395f0..dc253095dc 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -138,10 +138,6 @@ class MoveDatadiskDialog extends LitElement { ${device} ` )} - Test - Test - Test - Test ({ + (unit_of_measurement: string | undefined): NumberSelector => ({ number: { step: 0.01, unit_of_measurement, @@ -135,7 +136,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { } else { const data = this._stats5min.length >= 2 ? this._stats5min : this._statsHour; - const unit = this._params!.statistic.display_unit_of_measurement; + const unit = getDisplayUnit( + this.hass, + this._params!.statistic.statistic_id, + this._params!.statistic + ); const rows: TemplateResult[] = []; for (let i = 1; i < data.length; i++) { const stat = data[i]; @@ -192,6 +197,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { } private _renderAdjustStat() { + const unit = getDisplayUnit( + this.hass, + this._params!.statistic.statistic_id, + this._params!.statistic + ); return html`
Statistic: ${this._params!.statistic.statistic_id} @@ -220,9 +230,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { { @@ -299,6 +307,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { } private async _fixIssue(): Promise { + const unit = getDisplayUnit( + this.hass, + this._params!.statistic.statistic_id, + this._params!.statistic + ); this._busy = true; try { await adjustStatisticsSum( @@ -306,7 +319,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { this._params!.statistic.statistic_id, this._chosenStat!.start, this._amount! - this._origAmount!, - this._params!.statistic.display_unit_of_measurement + unit || null ); } catch (err: any) { this._busy = false; 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 d204562559..cb311b7637 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -315,8 +315,11 @@ class HuiEnergyDistrubutionCard ${formatNumber(gasUsage || 0, this.hass.locale, { maximumFractionDigits: 1, })} - ${getEnergyGasUnit(prefs, this._data.statsMetadata) || - "m³"} + ${getEnergyGasUnit( + this.hass, + prefs, + this._data.statsMetadata + ) || "m³"}
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 fc940d5605..2bd6855782 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 @@ -274,7 +274,8 @@ export class HuiEnergyGasGraphCard ) as GasSourceTypeEnergyPreference[]; this._unit = - getEnergyGasUnit(energyData.prefs, energyData.statsMetadata) || "m³"; + getEnergyGasUnit(this.hass, energyData.prefs, energyData.statsMetadata) || + "m³"; const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = []; 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 dc5eb4b977..3880736f53 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 @@ -130,7 +130,8 @@ export class HuiEnergySourcesTableCard ); const gasUnit = - getEnergyGasUnit(this._data.prefs, this._data.statsMetadata) || ""; + getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) || + ""; const compare = this._data.statsCompare !== 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 606b1d2166..e894f09411 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 @@ -205,7 +205,7 @@ export class HuiStatisticsGraphCardEditor ...this._config, stat_types: configured_stat_types, }; - const displayUnit = this._metaDatas?.[0]?.display_unit_of_measurement; + const unitClass = this._metaDatas?.[0]?.unit_class; return html` - `; - break; - case "tab-badges": - content = html` - ${this._badges?.length - ? html` - ${VIEWS_NO_BADGE_SUPPORT.includes(this._type) - ? html` - - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_badges.view_no_badges" - )} - - ` - : ""} -
- ${this._badges.map( - (badgeConfig) => html` - - ` - )} -
- ` - : ""} - - `; - break; - case "tab-visibility": - content = html` - - `; - break; - case "tab-cards": - content = html` Cards `; - break; + + if (this._yamlMode) { + content = html` + + `; + } else { + switch (this._curTab) { + case "tab-settings": + content = html` + + `; + break; + case "tab-badges": + content = html` + ${this._badges?.length + ? html` + ${VIEWS_NO_BADGE_SUPPORT.includes(this._type) + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_badges.view_no_badges" + )} + + ` + : ""} +
+ ${this._badges.map( + (badgeConfig) => html` + + ` + )} +
+ ` + : ""} + + `; + break; + case "tab-visibility": + content = html` + + `; + break; + case "tab-cards": + content = html` Cards `; + break; + } } + return html`

${this._viewConfigTitle}

- - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_view.tab_settings" - )} - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_view.tab_badges" - )} - ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_view.tab_visibility" - )} - + + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.edit_ui" + )} + ${!this._yamlMode + ? html`` + : ``} + + + + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.edit_yaml" + )} + ${this._yamlMode + ? html`` + : ``} + + + ${!this._yamlMode + ? html` + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.tab_settings" + )} + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.tab_badges" + )} + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_view.tab_visibility" + )} + ` + : ""}
${content} ${this._params.viewIndex !== undefined @@ -235,6 +318,19 @@ export class HuiDialogEditView extends LitElement { `; } + private async _handleAction(ev: CustomEvent) { + ev.stopPropagation(); + ev.preventDefault(); + switch (ev.detail.index) { + case 0: + this._yamlMode = false; + break; + case 1: + this._yamlMode = true; + break; + } + } + private async _delete(): Promise { if (!this._params) { return; @@ -348,6 +444,17 @@ export class HuiDialogEditView extends LitElement { this._dirty = true; } + private _viewYamlChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (!ev.detail.isValid) { + return; + } + const { badges = [], ...config } = ev.detail.value; + this._config = config; + this._badges = badges; + this._dirty = true; + } + private _isConfigChanged(): boolean { return ( this._creatingView || @@ -366,6 +473,9 @@ export class HuiDialogEditView extends LitElement { return [ haStyleDialog, css` + ha-dialog.yaml-mode { + --dialog-content-padding: 0; + } h2 { display: block; color: var(--primary-text-color); @@ -421,6 +531,22 @@ export class HuiDialogEditView extends LitElement { ha-circular-progress[active] { display: block; } + ha-button-menu { + color: var(--secondary-text-color); + position: absolute; + right: 16px; + top: 14px; + inset-inline-end: 16px; + inset-inline-start: initial; + direction: var(--direction); + } + ha-button-menu, + ha-icon-button { + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } + .selected_menu_item { + color: var(--primary-color); + } .hidden { display: none; } 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 e25345e36f..74b017bea6 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -33,7 +33,7 @@ export class HuiViewEditor extends LitElement { private _suggestedPath = false; private _schema = memoizeOne( - (localize: LocalizeFunc, subview: boolean, showAdvanced: boolean) => + (localize: LocalizeFunc) => [ { name: "title", selector: { text: {} } }, { @@ -69,14 +69,6 @@ export class HuiViewEditor extends LitElement { boolean: {}, }, }, - ...(subview && showAdvanced - ? [ - { - name: "back_path", - selector: { navigation: {} }, - }, - ] - : []), ] as const ); @@ -98,11 +90,7 @@ export class HuiViewEditor extends LitElement { return html``; } - const schema = this._schema( - this.hass.localize, - this._config.subview ?? false, - this.hass.userData?.showAdvanced ?? false - ); + const schema = this._schema(this.hass.localize); const data = { theme: "Backend-selected", @@ -128,9 +116,6 @@ export class HuiViewEditor extends LitElement { if (config.type === "masonry") { delete config.type; } - if (!config.subview) { - delete config.back_path; - } if ( this.isNew && @@ -155,10 +140,6 @@ export class HuiViewEditor extends LitElement { 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}` @@ -174,10 +155,6 @@ export class HuiViewEditor extends LitElement { 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; } diff --git a/src/translations/en.json b/src/translations/en.json index 3aeb0dffa6..84c1a70332 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1855,8 +1855,8 @@ "show_info": "Information", "default_name": "New Automation", "missing_name": "Cannot save automation without a name", + "traces_not_available": "Automations need an ID for history to be tracked. Add an ID to your automation to make it available in traces.", "load_error_not_editable": "Only automations in automations.yaml are editable.", - "load_error_not_duplicable": "Only automations in automations.yaml can be duplicated.", "load_error_not_deletable": "Only automations in automations.yaml can be deleted.", "load_error_unknown": "Error loading automation ({err_no}).", "save": "Save", @@ -2341,7 +2341,6 @@ "parallel": "Max number of parallel runs" }, "load_error_not_editable": "Only scripts inside scripts.yaml are editable.", - "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_title": "Delete script?", @@ -2610,6 +2609,7 @@ }, "devices": { "add_prompt": "No {name} have been added using this {type} yet. You can add one by clicking the + button above.", + "add_device": "Add Device", "caption": "Devices", "description": "Manage configured devices", "device_info": "{type} info", @@ -2985,8 +2985,6 @@ "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}.", "supported_hardware": "supported hardware", @@ -3059,7 +3057,9 @@ "editor": { "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", + "missing_credentials": "Setting up {integration} requires configuring application credentials.", + "missing_credentials_domain_link": "View {integration} documentation", + "view_documentation": "View application credentials documentation", "add": "Add", "domain": "Integration", "name": "Name", @@ -3804,8 +3804,8 @@ }, "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_ui": "Edit in visual editor", + "edit_yaml": "Edit in YAML" }, "edit_badges": { "view_no_badges": "Badges are not be supported by the current view type." diff --git a/src/util/brands-url.ts b/src/util/brands-url.ts index 7b0405b696..e5afa95320 100644 --- a/src/util/brands-url.ts +++ b/src/util/brands-url.ts @@ -3,6 +3,7 @@ export interface BrandsOptions { type: "icon" | "logo" | "icon@2x" | "logo@2x"; useFallback?: boolean; darkOptimized?: boolean; + brand?: boolean; } export interface HardwareBrandsOptions { @@ -13,9 +14,11 @@ export interface HardwareBrandsOptions { } export const brandsUrl = (options: BrandsOptions): string => - `https://brands.home-assistant.io/${options.useFallback ? "_/" : ""}${ - options.domain - }/${options.darkOptimized ? "dark_" : ""}${options.type}.png`; + `https://brands.home-assistant.io/${options.brand ? "brands/" : ""}${ + options.useFallback ? "_/" : "" + }${options.domain}/${options.darkOptimized ? "dark_" : ""}${ + options.type + }.png`; export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string => `https://brands.home-assistant.io/hardware/${options.category}/${