20221002.0 (#13953)

This commit is contained in:
Bram Kragten 2022-10-02 20:43:53 +02:00 committed by GitHub
commit e84b9b7c6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 666 additions and 439 deletions

View File

@ -36,6 +36,7 @@ const conditions = [
{ condition: "sun", after: "sunset" }, { condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise", offset: "-01:00" }, { condition: "sun", after: "sunrise", offset: "-01:00" },
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" }, { condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
{ condition: "trigger", id: "motion" },
{ condition: "time" }, { condition: "time" },
{ condition: "template" }, { condition: "template" },
]; ];

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20220929.0" version = "20221002.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -21,6 +21,7 @@ import {
numberFormatToLocale, numberFormatToLocale,
} from "../../common/number/format_number"; } from "../../common/number/format_number";
import { import {
getDisplayUnit,
getStatisticLabel, getStatisticLabel,
getStatisticMetadata, getStatisticMetadata,
Statistics, Statistics,
@ -258,8 +259,11 @@ class StatisticsChart extends LitElement {
if (!this.unit) { if (!this.unit) {
if (unit === undefined) { if (unit === undefined) {
unit = meta?.display_unit_of_measurement; unit = getDisplayUnit(this.hass, firstStat.statistic_id, meta);
} else if (unit !== meta?.display_unit_of_measurement) { } else if (
unit !== getDisplayUnit(this.hass, firstStat.statistic_id, meta)
) {
// Clear unit if not all statistics have same unit
unit = null; unit = null;
} }
} }

View File

@ -5,9 +5,12 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/ensure-array"; import { ensureArray } from "../../common/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeStateName } from "../../common/entity/compute_state_name";
import { stringCompare } from "../../common/string/compare"; 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 { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
@ -43,18 +46,11 @@ export class HaStatisticPicker extends LitElement {
public includeStatisticsUnitOfMeasurement?: string | string[]; public includeStatisticsUnitOfMeasurement?: string | string[];
/** /**
* Show only statistics displayed with these units of measurements. * Show only statistics with these unit classes.
* @attr include-display-unit-of-measurement * @attr include-unit-class
*/ */
@property({ attribute: "include-display-unit-of-measurement" }) @property({ attribute: "include-unit-class" })
public includeDisplayUnitOfMeasurement?: string | string[]; public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-classes
*/
@property({ attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/** /**
* Show only statistics on entities. * Show only statistics on entities.
@ -97,8 +93,7 @@ export class HaStatisticPicker extends LitElement {
( (
statisticIds: StatisticsMetaData[], statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[], includeStatisticsUnitOfMeasurement?: string | string[],
includeDisplayUnitOfMeasurement?: string | string[], includeUnitClass?: string | string[],
includeDeviceClasses?: string[],
entitiesOnly?: boolean entitiesOnly?: boolean
): Array<{ id: string; name: string; state?: HassEntity }> => { ): Array<{ id: string; name: string; state?: HassEntity }> => {
if (!statisticIds.length) { if (!statisticIds.length) {
@ -113,15 +108,18 @@ export class HaStatisticPicker extends LitElement {
} }
if (includeStatisticsUnitOfMeasurement) { if (includeStatisticsUnitOfMeasurement) {
const includeUnits = ensureArray(includeStatisticsUnitOfMeasurement); const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) => statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement) includeUnits.includes(meta.statistics_unit_of_measurement)
); );
} }
if (includeDisplayUnitOfMeasurement) { if (includeUnitClass) {
const includeUnits = ensureArray(includeDisplayUnitOfMeasurement); const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) => 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) { if (!entitiesOnly) {
output.push({ output.push({
id: meta.statistic_id, id: meta.statistic_id,
name: meta.name || meta.statistic_id, name: getStatisticLabel(this.hass, meta.statistic_id, meta),
}); });
} }
return; return;
} }
if ( output.push({
!includeDeviceClasses || id: meta.statistic_id,
includeDeviceClasses.includes( name: getStatisticLabel(this.hass, meta.statistic_id, meta),
entityState!.attributes.device_class || "" state: entityState,
) });
) {
output.push({
id: meta.statistic_id,
name: computeStateName(entityState),
state: entityState,
});
}
}); });
if (!output.length) { if (!output.length) {
@ -203,8 +194,7 @@ export class HaStatisticPicker extends LitElement {
(this.comboBox as any).items = this._getStatistics( (this.comboBox as any).items = this._getStatistics(
this.statisticIds!, this.statisticIds!,
this.includeStatisticsUnitOfMeasurement, this.includeStatisticsUnitOfMeasurement,
this.includeDisplayUnitOfMeasurement, this.includeUnitClass,
this.includeDeviceClasses,
this.entitiesOnly this.entitiesOnly
); );
} else { } else {
@ -212,8 +202,7 @@ export class HaStatisticPicker extends LitElement {
(this.comboBox as any).items = this._getStatistics( (this.comboBox as any).items = this._getStatistics(
this.statisticIds!, this.statisticIds!,
this.includeStatisticsUnitOfMeasurement, this.includeStatisticsUnitOfMeasurement,
this.includeDisplayUnitOfMeasurement, this.includeUnitClass,
this.includeDeviceClasses,
this.entitiesOnly this.entitiesOnly
); );
}); });

View File

@ -32,11 +32,11 @@ class HaStatisticsPicker extends LitElement {
public includeStatisticsUnitOfMeasurement?: string[] | string; public includeStatisticsUnitOfMeasurement?: string[] | string;
/** /**
* Show only statistics displayed with these units of measurements. * Show only statistics with these unit classes.
* @attr include-display-unit-of-measurement * @attr include-unit-class
*/ */
@property({ attribute: "include-display-unit-of-measurement" }) @property({ attribute: "include-unit-class" })
public includeDisplayUnitOfMeasurement?: string[] | string; public includeUnitClass?: string | string[];
/** /**
* Ignore filtering of statistics type and units when only a single statistic is selected. * 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.ignoreRestrictionsOnFirstStatistic &&
this._currentStatistics.length <= 1; this._currentStatistics.length <= 1;
const includeDisplayUnitCurrent = ignoreRestriction
? undefined
: this.includeDisplayUnitOfMeasurement;
const includeStatisticsUnitCurrent = ignoreRestriction const includeStatisticsUnitCurrent = ignoreRestriction
? undefined ? undefined
: this.includeStatisticsUnitOfMeasurement; : this.includeStatisticsUnitOfMeasurement;
const includeUnitClassCurrent = ignoreRestriction
? undefined
: this.includeUnitClass;
const includeStatisticTypesCurrent = ignoreRestriction const includeStatisticTypesCurrent = ignoreRestriction
? undefined ? undefined
: this.statisticTypes; : this.statisticTypes;
@ -75,8 +75,8 @@ class HaStatisticsPicker extends LitElement {
<ha-statistic-picker <ha-statistic-picker
.curValue=${statisticId} .curValue=${statisticId}
.hass=${this.hass} .hass=${this.hass}
.includeDisplayUnitOfMeasurement=${includeDisplayUnitCurrent}
.includeStatisticsUnitOfMeasurement=${includeStatisticsUnitCurrent} .includeStatisticsUnitOfMeasurement=${includeStatisticsUnitCurrent}
.includeUnitClass=${includeUnitClassCurrent}
.value=${statisticId} .value=${statisticId}
.statisticTypes=${includeStatisticTypesCurrent} .statisticTypes=${includeStatisticTypesCurrent}
.statisticIds=${this.statisticIds} .statisticIds=${this.statisticIds}
@ -89,10 +89,9 @@ class HaStatisticsPicker extends LitElement {
<div> <div>
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeDisplayUnitOfMeasurement=${this
.includeDisplayUnitOfMeasurement}
.includeStatisticsUnitOfMeasurement=${this .includeStatisticsUnitOfMeasurement=${this
.includeStatisticsUnitOfMeasurement} .includeStatisticsUnitOfMeasurement}
.includeUnitClass=${this.includeUnitClass}
.statisticTypes=${this.statisticTypes} .statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds} .statisticIds=${this.statisticIds}
.label=${this.pickStatisticLabel} .label=${this.pickStatisticLabel}

View File

@ -8,7 +8,14 @@ import type {
ComboBoxLightValueChangedEvent, ComboBoxLightValueChangedEvent,
} from "@vaadin/combo-box/vaadin-combo-box-light"; } from "@vaadin/combo-box/vaadin-combo-box-light";
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles"; import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
@ -225,11 +232,13 @@ export class HaComboBox extends LitElement {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail); fireEvent(this, ev.type, ev.detail);
if ( if (opened) {
opened && this.removeInertOnOverlay();
"MutationObserver" in window && }
!this._overlayMutationObserver }
) {
private removeInertOnOverlay() {
if ("MutationObserver" in window && !this._overlayMutationObserver) {
const overlay = document.querySelector<HTMLElement>( const overlay = document.querySelector<HTMLElement>(
"vaadin-combo-box-overlay" "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) { private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail, { composed: false }); fireEvent(this, ev.type, ev.detail, { composed: false });

View File

@ -13,7 +13,7 @@ type IconItem = {
icon: string; icon: string;
keywords: string[]; keywords: string[];
}; };
let iconItems: IconItem[] = [{ icon: "", keywords: [] }]; let iconItems: IconItem[] = [];
let iconLoaded = false; let iconLoaded = false;
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles

View File

@ -20,7 +20,7 @@ type NavigationItem = {
title: string; title: string;
}; };
const DEFAULT_ITEMS: NavigationItem[] = [{ path: "", icon: "", title: "" }]; const DEFAULT_ITEMS: NavigationItem[] = [];
// eslint-disable-next-line lit/prefer-static-styles // eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html` const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`

View File

@ -189,7 +189,11 @@ export const describeTrigger = (
// Time Trigger // Time Trigger
if (trigger.platform === "time" && trigger.at) { if (trigger.platform === "time" && trigger.at) {
const at = trigger.at.includes(".") 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; : trigger.at;
return `When the time is equal to ${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 `${ return `${
condition.condition ? condition.condition.replace(/_/g, " ") : "Unknown" condition.condition ? condition.condition.replace(/_/g, " ") : "Unknown"
} condition`; } condition`;

View File

@ -600,20 +600,14 @@ export const getEnergySolarForecasts = (hass: HomeAssistant) =>
type: "energy/solar_forecast", type: "energy/solar_forecast",
}); });
export const ENERGY_GAS_VOLUME_UNITS = ["m³"]; const energyGasUnitClass = ["volume", "energy"] as const;
export const ENERGY_GAS_ENERGY_UNITS = ["kWh"]; export type EnergyGasUnitClass = typeof energyGasUnitClass[number];
export const ENERGY_GAS_UNITS = [
...ENERGY_GAS_VOLUME_UNITS,
...ENERGY_GAS_ENERGY_UNITS,
];
export type EnergyGasUnit = "volume" | "energy"; export const getEnergyGasUnitClass = (
export const getEnergyGasUnitCategory = (
prefs: EnergyPreferences, prefs: EnergyPreferences,
statisticsMetaData: Record<string, StatisticsMetaData> = {}, statisticsMetaData: Record<string, StatisticsMetaData> = {},
excludeSource?: string excludeSource?: string
): EnergyGasUnit | undefined => { ): EnergyGasUnitClass | undefined => {
for (const source of prefs.energy_sources) { for (const source of prefs.energy_sources) {
if (source.type !== "gas") { if (source.type !== "gas") {
continue; continue;
@ -622,29 +616,29 @@ export const getEnergyGasUnitCategory = (
continue; continue;
} }
const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from]; const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
if (statisticIdWithMeta) { if (
return ENERGY_GAS_VOLUME_UNITS.includes( energyGasUnitClass.includes(
statisticIdWithMeta.statistics_unit_of_measurement statisticIdWithMeta.unit_class as EnergyGasUnitClass
) )
? "volume" ) {
: "energy"; return statisticIdWithMeta.unit_class as EnergyGasUnitClass;
} }
} }
return undefined; return undefined;
}; };
export const getEnergyGasUnit = ( export const getEnergyGasUnit = (
hass: HomeAssistant,
prefs: EnergyPreferences, prefs: EnergyPreferences,
statisticsMetaData: Record<string, StatisticsMetaData> = {} statisticsMetaData: Record<string, StatisticsMetaData> = {}
): string | undefined => { ): string | undefined => {
for (const source of prefs.energy_sources) { const unitClass = getEnergyGasUnitClass(prefs, statisticsMetaData);
if (source.type !== "gas") { if (unitClass === undefined) {
continue; return undefined;
}
const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
if (statisticIdWithMeta?.display_unit_of_measurement) {
return statisticIdWithMeta.display_unit_of_measurement;
}
} }
return undefined; return unitClass === "energy"
? "kWh"
: hass.config.unit_system.length === "km"
? "m³"
: "ft³";
}; };

View File

@ -20,13 +20,13 @@ export interface StatisticValue {
} }
export interface StatisticsMetaData { export interface StatisticsMetaData {
display_unit_of_measurement: string; statistics_unit_of_measurement: string | null;
statistics_unit_of_measurement: string;
statistic_id: string; statistic_id: string;
source: string; source: string;
name?: string | null; name?: string | null;
has_sum: boolean; has_sum: boolean;
has_mean: boolean; has_mean: boolean;
unit_class: string | null;
} }
export type StatisticsValidationResult = export type StatisticsValidationResult =
@ -254,14 +254,14 @@ export const adjustStatisticsSum = (
statistic_id: string, statistic_id: string,
start_time: string, start_time: string,
adjustment: number, adjustment: number,
display_unit: string adjustment_unit_of_measurement: string | null
): Promise<void> => ): Promise<void> =>
hass.callWS({ hass.callWS({
type: "recorder/adjust_sum_statistics", type: "recorder/adjust_sum_statistics",
statistic_id, statistic_id,
start_time, start_time,
adjustment, adjustment,
display_unit, adjustment_unit_of_measurement,
}); });
export const getStatisticLabel = ( export const getStatisticLabel = (
@ -275,3 +275,17 @@ export const getStatisticLabel = (
} }
return statisticsMetaData?.name || statisticsId; 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;
};

View File

@ -14,8 +14,6 @@ import { showAddApplicationCredentialDialog } from "../../panels/config/applicat
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow"; import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import { showConfigFlowDialog } from "./show-dialog-config-flow"; import { showConfigFlowDialog } from "./show-dialog-config-flow";
import { domainToName } from "../../data/integration";
import { showConfirmationDialog } from "../generic/show-dialog-box";
@customElement("step-flow-abort") @customElement("step-flow-abort")
class StepFlowAbort extends LitElement { class StepFlowAbort extends LitElement {
@ -54,26 +52,11 @@ class StepFlowAbort extends LitElement {
} }
private async _handleMissingCreds() { 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(); this._flowDone();
if (!confirm) {
return;
}
// Prompt to enter credentials and restart integration setup // Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, { showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain, selectedDomain: this.domain,
manifest: this.params.manifest,
applicationCredentialAddedCallback: () => { applicationCredentialAddedCallback: () => {
showConfigFlowDialog(this.params.dialogParentElement!, { showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback, dialogClosedCallback: this.params.dialogClosedCallback,

View File

@ -5,6 +5,7 @@ import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box"; import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
@ -16,7 +17,7 @@ import {
createApplicationCredential, createApplicationCredential,
fetchApplicationCredentialsConfig, fetchApplicationCredentialsConfig,
} from "../../../data/application_credential"; } from "../../../data/application_credential";
import { domainToName } from "../../../data/integration"; import { domainToName, IntegrationManifest } from "../../../data/integration";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
@ -44,6 +45,8 @@ export class DialogAddApplicationCredential extends LitElement {
@state() private _domain?: string; @state() private _domain?: string;
@state() private _manifest?: IntegrationManifest | null;
@state() private _name?: string; @state() private _name?: string;
@state() private _description?: string; @state() private _description?: string;
@ -58,8 +61,8 @@ export class DialogAddApplicationCredential extends LitElement {
public showDialog(params: AddApplicationCredentialDialogParams) { public showDialog(params: AddApplicationCredentialDialogParams) {
this._params = params; this._params = params;
this._domain = this._domain = params.selectedDomain;
params.selectedDomain !== undefined ? params.selectedDomain : ""; this._manifest = params.manifest;
this._name = ""; this._name = "";
this._description = ""; this._description = "";
this._clientId = ""; this._clientId = "";
@ -76,7 +79,7 @@ export class DialogAddApplicationCredential extends LitElement {
name: domainToName(this.hass.localize, domain), name: domainToName(this.hass.localize, domain),
})); }));
await this.hass.loadBackendTranslation("application_credentials"); await this.hass.loadBackendTranslation("application_credentials");
if (this._domain !== "") { if (this._domain) {
this._updateDescription(); this._updateDescription();
} }
} }
@ -85,6 +88,9 @@ export class DialogAddApplicationCredential extends LitElement {
if (!this._params || !this._domains) { if (!this._params || !this._domains) {
return html``; return html``;
} }
const selectedDomainName = this._params.selectedDomain
? domainToName(this.hass.localize, this._domain!)
: "";
return html` return html`
<ha-dialog <ha-dialog
open open
@ -99,42 +105,76 @@ export class DialogAddApplicationCredential extends LitElement {
)} )}
> >
<div> <div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error
<p> ? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
${this.hass.localize( : ""}
"ui.panel.config.application_credentials.editor.description" ${this._params.selectedDomain && !this._description
)} ? html`<p>
<br /> ${this.hass.localize(
<a "ui.panel.config.application_credentials.editor.missing_credentials",
href=${documentationUrl( {
this.hass!, integration: selectedDomainName,
"/integrations/application_credentials" }
)} )}
target="_blank" ${this._manifest?.is_built_in || this._manifest?.documentation
rel="noreferrer" ? html`<a
> href=${this._manifest.is_built_in
${this.hass!.localize( ? documentationUrl(
"ui.panel.config.application_credentials.editor.view_documentation" this.hass,
)} `/integrations/${this._domain}`
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon> )
</a> : this._manifest.documentation}
</p> target="_blank"
<ha-combo-box rel="noreferrer"
name="domain" >
.hass=${this.hass} ${this.hass.localize(
.disabled=${!!this._params.selectedDomain} "ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
.label=${this.hass.localize( {
"ui.panel.config.application_credentials.editor.domain" integration: selectedDomainName,
)} }
.value=${this._domain} )}
.renderer=${rowRenderer} <ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
.items=${this._domains} </a>`
item-id-path="id" : ""}
item-value-path="id" </p>`
item-label-path="name" : ""}
required ${!this._params.selectedDomain || !this._description
@value-changed=${this._handleDomainPicked} ? html`<p>
></ha-combo-box> ${this.hass.localize(
"ui.panel.config.application_credentials.editor.description"
)}
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>`
: ""}
${this._params.selectedDomain
? ""
: html`<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.renderer=${rowRenderer}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
${this._description ${this._description
? html`<ha-markdown ? html`<ha-markdown
breaks breaks
@ -223,7 +263,11 @@ export class DialogAddApplicationCredential extends LitElement {
this._updateDescription(); this._updateDescription();
} }
private _updateDescription() { private async _updateDescription() {
await this.hass.loadBackendTranslation(
"application_credentials",
this._domain
);
const info = this._config!.integrations[this._domain!]; const info = this._config!.integrations[this._domain!];
this._description = this.hass.localize( this._description = this.hass.localize(
`component.${this._domain}.application_credentials.description`, `component.${this._domain}.application_credentials.description`,
@ -298,6 +342,9 @@ export class DialogAddApplicationCredential extends LitElement {
a ha-svg-icon { a ha-svg-icon {
--mdc-icon-size: 16px; --mdc-icon-size: 16px;
} }
ha-markdown {
margin-bottom: 16px;
}
`, `,
]; ];
} }

View File

@ -1,5 +1,6 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { ApplicationCredential } from "../../../data/application_credential"; import { ApplicationCredential } from "../../../data/application_credential";
import { IntegrationManifest } from "../../../data/integration";
export interface AddApplicationCredentialDialogParams { export interface AddApplicationCredentialDialogParams {
applicationCredentialAddedCallback: ( applicationCredentialAddedCallback: (
@ -7,6 +8,7 @@ export interface AddApplicationCredentialDialogParams {
) => void; ) => void;
dialogAbortedCallback?: () => void; dialogAbortedCallback?: () => void;
selectedDomain?: string; selectedDomain?: string;
manifest?: IntegrationManifest | null;
} }
export const loadAddApplicationCredentialDialog = () => export const loadAddApplicationCredentialDialog = () =>

View File

@ -55,7 +55,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h3> </h3>
<ha-automation-condition <ha-automation-condition
.conditions=${option.conditions} .conditions=${ensureArray<string | Condition>(option.conditions)}
.reOrderMode=${this.reOrderMode} .reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
@ -68,7 +68,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h3> </h3>
<ha-automation-action <ha-automation-action
.actions=${option.sequence || []} .actions=${ensureArray(option.sequence) || []}
.reOrderMode=${this.reOrderMode} .reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
@ -96,7 +96,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h2> </h2>
<ha-automation-action <ha-automation-action
.actions=${action.default || []} .actions=${ensureArray(action.default) || []}
.reOrderMode=${this.reOrderMode} .reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._defaultChanged} @value-changed=${this._defaultChanged}

View File

@ -444,7 +444,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
if (changedProps.has("entityId") && this.entityId) { if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => { getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this._config = c.config; this._config = this._normalizeConfig(c.config);
}); });
this._entityId = this.entityId; this._entityId = this.entityId;
this._dirty = false; this._dirty = false;
@ -473,24 +473,27 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._entityId = automation?.entity_id; 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() { private async _loadConfig() {
try { try {
const config = await fetchAutomationFileConfig( const config = await fetchAutomationFileConfig(
this.hass, this.hass,
this.automationId as string 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._dirty = false;
this._readOnly = false; this._readOnly = false;
this._config = config; this._config = this._normalizeConfig(config);
} catch (err: any) { } catch (err: any) {
const entity = Object.values(this.hass.entities).find( const entity = Object.values(this.hass.entities).find(
(ent) => (ent) =>

View File

@ -35,6 +35,8 @@ import {
deleteAutomation, deleteAutomation,
duplicateAutomation, duplicateAutomation,
fetchAutomationFileConfig, fetchAutomationFileConfig,
getAutomationStateConfig,
showAutomationEditor,
triggerAutomationActions, triggerAutomationActions,
} from "../../../data/automation"; } from "../../../data/automation";
import { import {
@ -329,6 +331,14 @@ class HaAutomationPicker extends LitElement {
} }
private _showTrace(automation: any) { 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}`); navigate(`/config/automation/trace/${automation.attributes.id}`);
} }
@ -382,17 +392,20 @@ class HaAutomationPicker extends LitElement {
); );
duplicateAutomation(config); duplicateAutomation(config);
} catch (err: any) { } 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, { await showAlertDialog(this, {
text: text: this.hass.localize(
err.status_code === 404 "ui.panel.config.automation.editor.load_error_unknown",
? this.hass.localize( "err_no",
"ui.panel.config.automation.editor.load_error_not_duplicable" err.status_code
) ),
: this.hass.localize(
"ui.panel.config.automation.editor.load_error_unknown",
"err_no",
err.status_code
),
}); });
} }
} }

View File

@ -38,6 +38,7 @@ import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu"; import "../integrations/ha-integration-overflow-menu";
import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; 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 { interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData; device?: DeviceRowData;
@ -363,16 +364,15 @@ export class HaConfigDeviceDashboard extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const { devicesOutput, filteredConfigEntry } = const { devicesOutput } = this._devicesAndFilterDomains(
this._devicesAndFilterDomains( this.devices,
this.devices, this.entries,
this.entries, this.entities,
this.entities, this.areas,
this.areas, this._searchParms,
this._searchParms, this._showDisabled,
this._showDisabled, this.hass.localize
this.hass.localize );
);
const activeFilters = this._activeFilters( const activeFilters = this._activeFilters(
this.entries, this.entries,
this._searchParms, this._searchParms,
@ -405,39 +405,21 @@ export class HaConfigDeviceDashboard extends LitElement {
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
clickable clickable
.hasFab=${filteredConfigEntry && hasFab
(filteredConfigEntry.domain === "zha" ||
filteredConfigEntry.domain === "zwave_js")}
> >
<ha-integration-overflow-menu <ha-integration-overflow-menu
.hass=${this.hass} .hass=${this.hass}
slot="toolbar-icon" slot="toolbar-icon"
></ha-integration-overflow-menu> ></ha-integration-overflow-menu>
${!filteredConfigEntry <ha-fab
? "" slot="fab"
: filteredConfigEntry.domain === "zwave_js" .label=${this.hass.localize("ui.panel.config.devices.add_device")}
? html` extended
<ha-fab @click=${this._addDevice}
slot="fab" ?rtl=${computeRTL(this.hass)}
.label=${this.hass.localize("ui.panel.config.zha.add_device")} >
extended <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
?rtl=${computeRTL(this.hass)} </ha-fab>
@click=${this._showZJSAddDeviceDialog}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: filteredConfigEntry.domain === "zha"
? html`<a href="/config/zha/add" slot="fab">
<ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended
?rtl=${computeRTL(this.hass)}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>`
: html``}
<ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi> <ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
@ -516,7 +498,7 @@ export class HaConfigDeviceDashboard extends LitElement {
this._showDisabled = true; this._showDisabled = true;
} }
private _showZJSAddDeviceDialog() { private _addDevice() {
const { filteredConfigEntry } = this._devicesAndFilterDomains( const { filteredConfigEntry } = this._devicesAndFilterDomains(
this.devices, this.devices,
this.entries, this.entries,
@ -526,7 +508,18 @@ export class HaConfigDeviceDashboard extends LitElement {
this._showDisabled, this._showDisabled,
this.hass.localize this.hass.localize
); );
if (filteredConfigEntry?.domain === "zha") {
navigate(`/config/zha/add`);
return;
}
if (filteredConfigEntry?.domain === "zwave_js") {
this._showZJSAddDeviceDialog(filteredConfigEntry);
return;
}
showAddIntegrationDialog(this);
}
private _showZJSAddDeviceDialog(filteredConfigEntry: ConfigEntry) {
showZWaveJSAddNodeDialog(this, { showZWaveJSAddNodeDialog(this, {
entry_id: filteredConfigEntry!.entry_id, entry_id: filteredConfigEntry!.entry_id,
}); });

View File

@ -10,7 +10,7 @@ import {
EnergyPreferencesValidation, EnergyPreferencesValidation,
EnergyValidationIssue, EnergyValidationIssue,
GasSourceTypeEnergyPreference, GasSourceTypeEnergyPreference,
getEnergyGasUnitCategory, getEnergyGasUnitClass,
saveEnergyPreferences, saveEnergyPreferences,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { import {
@ -133,7 +133,7 @@ export class EnergyGasSettings extends LitElement {
private _addSource() { private _addSource() {
showEnergySettingsGasDialog(this, { showEnergySettingsGasDialog(this, {
allowedGasUnitCategory: getEnergyGasUnitCategory( allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences, this.preferences,
this.statsMetadata this.statsMetadata
), ),
@ -152,7 +152,7 @@ export class EnergyGasSettings extends LitElement {
ev.currentTarget.closest(".row").source; ev.currentTarget.closest(".row").source;
showEnergySettingsGasDialog(this, { showEnergySettingsGasDialog(this, {
source: { ...origSource }, source: { ...origSource },
allowedGasUnitCategory: getEnergyGasUnitCategory( allowedGasUnitClass: getEnergyGasUnitClass(
this.preferences, this.preferences,
this.statsMetadata, this.statsMetadata,
origSource.stat_energy_from origSource.stat_energy_from

View File

@ -14,8 +14,7 @@ import { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/entity/ha-statistic-picker";
const energyUnits = ["kWh"]; const energyUnitClasses = ["energy"];
const energyDeviceClasses = ["energy"];
@customElement("dialog-energy-battery-settings") @customElement("dialog-energy-battery-settings")
export class DialogEnergyBatterySettings export class DialogEnergyBatterySettings
@ -67,8 +66,7 @@ export class DialogEnergyBatterySettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${energyUnits} .includeUnitClass=${energyUnitClasses}
.includeDeviceClasses=${energyDeviceClasses}
.value=${this._source.stat_energy_to} .value=${this._source.stat_energy_to}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_into_battery" "ui.panel.config.energy.battery.dialog.energy_into_battery"
@ -79,8 +77,7 @@ export class DialogEnergyBatterySettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${energyUnits} .includeUnitClass=${energyUnitClasses}
.includeDeviceClasses=${energyDeviceClasses}
.value=${this._source.stat_energy_from} .value=${this._source.stat_energy_from}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_out_of_battery" "ui.panel.config.energy.battery.dialog.energy_out_of_battery"

View File

@ -14,8 +14,7 @@ import "../../../../components/ha-radio";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
const energyUnits = ["kWh"]; const energyUnitClasses = ["energy"];
const energyDeviceClasses = ["energy"];
@customElement("dialog-energy-device-settings") @customElement("dialog-energy-device-settings")
export class DialogEnergyDeviceSettings export class DialogEnergyDeviceSettings
@ -69,8 +68,7 @@ export class DialogEnergyDeviceSettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${energyUnits} .includeUnitClass=${energyUnitClasses}
.includeDeviceClasses=${energyDeviceClasses}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.device_consumption_energy" "ui.panel.config.energy.device_consumption.dialog.device_consumption_energy"
)} )}

View File

@ -5,9 +5,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog";
import { import {
emptyGasEnergyPreference, emptyGasEnergyPreference,
ENERGY_GAS_ENERGY_UNITS,
ENERGY_GAS_UNITS,
ENERGY_GAS_VOLUME_UNITS,
GasSourceTypeEnergyPreference, GasSourceTypeEnergyPreference,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { HassDialog } from "../../../../dialogs/make-dialog-manager";
@ -21,7 +18,10 @@ import "../../../../components/ha-radio";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import type { HaRadio } from "../../../../components/ha-radio"; import type { HaRadio } from "../../../../components/ha-radio";
import { getStatisticMetadata } from "../../../../data/recorder"; import {
getStatisticMetadata,
getDisplayUnit,
} from "../../../../data/recorder";
@customElement("dialog-energy-gas-settings") @customElement("dialog-energy-gas-settings")
export class DialogEnergyGasSettings export class DialogEnergyGasSettings
@ -38,7 +38,7 @@ export class DialogEnergyGasSettings
@state() private _pickableUnit?: string; @state() private _pickableUnit?: string;
@state() private _pickedDisplayUnit?: string; @state() private _pickedDisplayUnit?: string | null;
@state() private _error?: string; @state() private _error?: string;
@ -49,7 +49,11 @@ export class DialogEnergyGasSettings
this._source = params.source this._source = params.source
? { ...params.source } ? { ...params.source }
: emptyGasEnergyPreference(); : emptyGasEnergyPreference();
this._pickedDisplayUnit = params.metadata?.display_unit_of_measurement; this._pickedDisplayUnit = getDisplayUnit(
this.hass,
params.source?.stat_energy_from,
params.metadata
);
this._costs = this._source.entity_energy_price this._costs = this._source.entity_energy_price
? "entity" ? "entity"
: this._source.number_energy_price : this._source.number_energy_price
@ -75,9 +79,9 @@ export class DialogEnergyGasSettings
const pickableUnit = const pickableUnit =
this._pickableUnit || this._pickableUnit ||
(this._params.allowedGasUnitCategory === undefined (this._params.allowedGasUnitClass === undefined
? "ft³, m³, Wh, kWh or MWh" ? "ft³, m³, Wh, kWh or MWh"
: this._params.allowedGasUnitCategory === "energy" : this._params.allowedGasUnitClass === "energy"
? "Wh, kWh or MWh" ? "Wh, kWh or MWh"
: "ft³ or m³"); : "ft³ or m³");
@ -98,17 +102,12 @@ export class DialogEnergyGasSettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${this._params .includeUnitClass=${this._params.allowedGasUnitClass}
.allowedGasUnitCategory === undefined
? ENERGY_GAS_UNITS
: this._params.allowedGasUnitCategory === "energy"
? ENERGY_GAS_ENERGY_UNITS
: ENERGY_GAS_VOLUME_UNITS}
.value=${this._source.stat_energy_from} .value=${this._source.stat_energy_from}
.label=${`${this.hass.localize( .label=${`${this.hass.localize(
"ui.panel.config.energy.gas.dialog.gas_usage" "ui.panel.config.energy.gas.dialog.gas_usage"
)} (${ )} (${
this._params.allowedGasUnitCategory === undefined this._params.allowedGasUnitClass === undefined
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.energy.gas.dialog.m3_or_kWh" "ui.panel.config.energy.gas.dialog.m3_or_kWh"
) )
@ -263,14 +262,12 @@ export class DialogEnergyGasSettings
private async _statisticChanged(ev: CustomEvent<{ value: string }>) { private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) { if (ev.detail.value) {
const entity = this.hass.states[ev.detail.value]; const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
if (entity?.attributes.unit_of_measurement) { this._pickedDisplayUnit = getDisplayUnit(
this._pickedDisplayUnit = entity.attributes.unit_of_measurement; this.hass,
} else { ev.detail.value,
this._pickedDisplayUnit = ( metadata[0]
await getStatisticMetadata(this.hass, [ev.detail.value]) );
)[0]?.display_unit_of_measurement;
}
} else { } else {
this._pickedDisplayUnit = undefined; this._pickedDisplayUnit = undefined;
} }

View File

@ -20,8 +20,7 @@ import "../../../../components/ha-formfield";
import type { HaRadio } from "../../../../components/ha-radio"; import type { HaRadio } from "../../../../components/ha-radio";
import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-entity-picker";
const energyUnits = ["kWh"]; const energyUnitClasses = ["energy"];
const energyDeviceClasses = ["energy"];
@customElement("dialog-energy-grid-flow-settings") @customElement("dialog-energy-grid-flow-settings")
export class DialogEnergyGridFlowSettings export class DialogEnergyGridFlowSettings
@ -93,8 +92,7 @@ export class DialogEnergyGridFlowSettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${energyUnits} .includeUnitClass=${energyUnitClasses}
.includeDeviceClasses=${energyDeviceClasses}
.value=${this._source[ .value=${this._source[
this._params.direction === "from" this._params.direction === "from"
? "stat_energy_from" ? "stat_energy_from"

View File

@ -22,8 +22,7 @@ import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialo
import { ConfigEntry, getConfigEntries } from "../../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../../data/config_entries";
import { brandsUrl } from "../../../../util/brands-url"; import { brandsUrl } from "../../../../util/brands-url";
const energyUnits = ["kWh"]; const energyUnitClasses = ["energy"];
const energyDeviceClasses = ["energy"];
@customElement("dialog-energy-solar-settings") @customElement("dialog-energy-solar-settings")
export class DialogEnergySolarSettings export class DialogEnergySolarSettings
@ -79,8 +78,7 @@ export class DialogEnergySolarSettings
<ha-statistic-picker <ha-statistic-picker
.hass=${this.hass} .hass=${this.hass}
.includeStatisticsUnitOfMeasurement=${energyUnits} .includeUnitClass=${energyUnitClasses}
.includeDeviceClasses=${energyDeviceClasses}
.value=${this._source.stat_energy_from} .value=${this._source.stat_energy_from}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_energy" "ui.panel.config.energy.solar.dialog.solar_production_energy"

View File

@ -2,7 +2,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { import {
BatterySourceTypeEnergyPreference, BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference, DeviceConsumptionEnergyPreference,
EnergyGasUnit, EnergyGasUnitClass,
EnergyInfo, EnergyInfo,
FlowFromGridSourceEnergyPreference, FlowFromGridSourceEnergyPreference,
FlowToGridSourceEnergyPreference, FlowToGridSourceEnergyPreference,
@ -46,7 +46,7 @@ export interface EnergySettingsBatteryDialogParams {
export interface EnergySettingsGasDialogParams { export interface EnergySettingsGasDialogParams {
source?: GasSourceTypeEnergyPreference; source?: GasSourceTypeEnergyPreference;
allowedGasUnitCategory?: EnergyGasUnit; allowedGasUnitClass?: EnergyGasUnitClass;
metadata?: StatisticsMetaData; metadata?: StatisticsMetaData;
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>; saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
} }

View File

@ -219,19 +219,18 @@ class HaScheduleForm extends LitElement {
const start = new Date(); const start = new Date();
start.setDate(start.getDate() + distance); start.setDate(start.getDate() + distance);
const start_tokens = item.from.split(":");
start.setHours( start.setHours(
parseInt(item.from.slice(0, 2)), parseInt(start_tokens[0]),
parseInt(item.from.slice(-2)) parseInt(start_tokens[1]),
0,
0
); );
const end = new Date(); const end = new Date();
end.setDate(end.getDate() + distance); end.setDate(end.getDate() + distance);
end.setHours( const end_tokens = item.to.split(":");
parseInt(item.to.slice(0, 2)), end.setHours(parseInt(end_tokens[0]), parseInt(end_tokens[1]), 0, 0);
parseInt(item.to.slice(-2)),
0,
0
);
events.push({ events.push({
id: `${day}-${index}`, id: `${day}-${index}`,

View File

@ -80,10 +80,10 @@ class AddIntegrationDialog extends LitElement {
private _height?: number; private _height?: number;
public showDialog(params: AddIntegrationDialogParams): void { public showDialog(params?: AddIntegrationDialogParams): void {
this._open = true; this._open = true;
this._pickedBrand = params.brand; this._pickedBrand = params?.brand;
this._initialFilter = params.initialFilter; this._initialFilter = params?.initialFilter;
this._narrow = matchMedia( this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)" "all and (max-width: 450px), all and (max-height: 500px)"
).matches; ).matches;
@ -296,14 +296,13 @@ class AddIntegrationDialog extends LitElement {
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions hideActions
.heading=${this._pickedBrand .heading=${createCloseHeading(
? true this.hass,
: createCloseHeading( this.hass.localize("ui.panel.config.integrations.new")
this.hass, )}
this.hass.localize("ui.panel.config.integrations.new")
)}
> >
${this._pickedBrand ${this._pickedBrand &&
(!this._integrations || this._pickedBrand in this._integrations)
? html`<div slot="heading"> ? html`<div slot="heading">
<ha-icon-button-prev <ha-icon-button-prev
@click=${this._prevClicked} @click=${this._prevClicked}
@ -385,7 +384,11 @@ class AddIntegrationDialog extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-integration-list-item .hass=${this.hass} .integration=${integration}> <ha-integration-list-item
brand
.hass=${this.hass}
.integration=${integration}
>
</ha-integration-list-item> </ha-integration-list-item>
`; `;
}; };
@ -506,7 +509,16 @@ class AddIntegrationDialog extends LitElement {
if (integration.integrations) { if (integration.integrations) {
const integrations = const integrations =
this._integrations![integration.domain].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; this._pickedBrand = integration.domain;
return; return;
} }
@ -529,6 +541,15 @@ class AddIntegrationDialog extends LitElement {
return; return;
} }
if (
["cloud", "google_assistant", "alexa"].includes(integration.domain) &&
isComponentLoaded(this.hass, "cloud")
) {
this.closeDialog();
navigate("/config/cloud");
return;
}
const manifest = await fetchIntegrationManifest( const manifest = await fetchIntegrationManifest(
this.hass, this.hass,
integration.domain integration.domain
@ -591,7 +612,14 @@ class AddIntegrationDialog extends LitElement {
private async _fetchFlowsInProgress(domains: string[]) { private async _fetchFlowsInProgress(domains: string[]) {
const flowsInProgress = ( const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection) 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) { if (flowsInProgress.length) {
this._flowsInProgress = flowsInProgress; this._flowsInProgress = flowsInProgress;

View File

@ -1,7 +1,9 @@
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { localizeConfigFlowTitle } from "../../../data/config_flow"; import { localizeConfigFlowTitle } from "../../../data/config_flow";
import { DataEntryFlowProgress } from "../../../data/data_entry_flow"; import { DataEntryFlowProgress } from "../../../data/data_entry_flow";
@ -26,7 +28,7 @@ class HaDomainIntegrations extends LitElement {
@property() public domain!: string; @property() public domain!: string;
@property({ attribute: false }) public integration!: Integration; @property({ attribute: false }) public integration?: Integration;
@property({ attribute: false }) @property({ attribute: false })
public flowsInProgress?: DataEntryFlowProgress[]; public flowsInProgress?: DataEntryFlowProgress[];
@ -179,9 +181,22 @@ class HaDomainIntegrations extends LitElement {
private async _integrationPicked(ev) { private async _integrationPicked(ev) {
const domain = ev.currentTarget.domain; const domain = ev.currentTarget.domain;
if ( if (
(domain === this.domain && !this.integration.config_flow) || ["cloud", "google_assistant", "alexa"].includes(domain) &&
!this.integration.integrations?.[domain]?.config_flow 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); const manifest = await fetchIntegrationManifest(this.hass, domain);
showAlertDialog(this, { showAlertDialog(this, {

View File

@ -22,6 +22,8 @@ export class HaIntegrationListItem extends ListItemBase {
@property({ type: Boolean }) hasMeta = true; @property({ type: Boolean }) hasMeta = true;
@property({ type: Boolean }) brand = false;
renderSingleLine() { renderSingleLine() {
if (!this.integration) { if (!this.integration) {
return html``; return html``;
@ -51,6 +53,7 @@ export class HaIntegrationListItem extends ListItemBase {
type: "icon", type: "icon",
useFallback: true, useFallback: true,
darkOptimized: this.hass.themes?.darkMode, darkOptimized: this.hass.themes?.darkMode,
brand: this.brand,
})} })}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View File

@ -469,15 +469,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
) { ) {
fetchScriptFileConfig(this.hass, this.scriptId).then( fetchScriptFileConfig(this.hass, this.scriptId).then(
(config) => { (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._dirty = false;
this._readOnly = false; this._readOnly = false;
this._config = config; this._config = this._normalizeConfig(config);
}, },
(resp) => { (resp) => {
const entity = Object.values(this.hass.entities).find( const entity = Object.values(this.hass.entities).find(
@ -524,7 +518,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
if (changedProps.has("entityId") && this.entityId) { if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => { getScriptStateConfig(this.hass, this.entityId).then((c) => {
this._config = c.config; this._config = this._normalizeConfig(c.config);
}); });
const regEntry = this.hass.entities[this.entityId]; const regEntry = this.hass.entities[this.entityId];
if (regEntry?.unique_id) { 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 = ( private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>, schema: SchemaUnion<ReturnType<typeof this._schema>>,
data: HaFormDataContainer data: HaFormDataContainer

View File

@ -28,6 +28,7 @@ import "../../../components/ha-svg-icon";
import { import {
deleteScript, deleteScript,
fetchScriptFileConfig, fetchScriptFileConfig,
getScriptStateConfig,
showScriptEditor, showScriptEditor,
triggerScript, triggerScript,
} from "../../../data/script"; } from "../../../data/script";
@ -311,17 +312,20 @@ class HaScriptPicker extends LitElement {
)})`, )})`,
}); });
} catch (err: any) { } catch (err: any) {
if (err.status_code === 404) {
const response = await getScriptStateConfig(
this.hass,
script.entity_id
);
showScriptEditor(response.config);
return;
}
await showAlertDialog(this, { await showAlertDialog(this, {
text: text: this.hass.localize(
err.status_code === 404 "ui.panel.config.script.editor.load_error_unknown",
? this.hass.localize( "err_no",
"ui.panel.config.script.editor.load_error_not_duplicable" err.status_code
) ),
: this.hass.localize(
"ui.panel.config.script.editor.load_error_unknown",
"err_no",
err.status_code
),
}); });
} }
} }

View File

@ -138,10 +138,6 @@ class MoveDatadiskDialog extends LitElement {
${device} ${device}
</mwc-list-item>` </mwc-list-item>`
)} )}
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
<mwc-list-item>Test</mwc-list-item>
</ha-select> </ha-select>
<mwc-button <mwc-button

View File

@ -74,12 +74,6 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
hidden: this.narrow, hidden: this.narrow,
width: "20%", width: "20%",
}, },
display_unit_of_measurement: {
title: "Display unit",
sortable: true,
filterable: true,
width: "10%",
},
statistics_unit_of_measurement: { statistics_unit_of_measurement: {
title: "Statistics unit", title: "Statistics unit",
sortable: true, sortable: true,
@ -220,12 +214,12 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
this._data.push({ this._data.push({
statistic_id: statisticId, statistic_id: statisticId,
statistics_unit_of_measurement: "", statistics_unit_of_measurement: "",
display_unit_of_measurement: "",
source: "", source: "",
state: this.hass.states[statisticId], state: this.hass.states[statisticId],
issues: issues[statisticId], issues: issues[statisticId],
has_mean: false, has_mean: false,
has_sum: false, has_sum: false,
unit_class: null,
}); });
} }
}); });

View File

@ -23,6 +23,7 @@ import "../../../components/ha-svg-icon";
import { import {
adjustStatisticsSum, adjustStatisticsSum,
fetchStatistics, fetchStatistics,
getDisplayUnit,
StatisticValue, StatisticValue,
} from "../../../data/recorder"; } from "../../../data/recorder";
import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; import type { DateTimeSelector, NumberSelector } from "../../../data/selector";
@ -59,7 +60,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
}; };
private _amountSelector = memoizeOne( private _amountSelector = memoizeOne(
(unit_of_measurement: string): NumberSelector => ({ (unit_of_measurement: string | undefined): NumberSelector => ({
number: { number: {
step: 0.01, step: 0.01,
unit_of_measurement, unit_of_measurement,
@ -135,7 +136,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
} else { } else {
const data = const data =
this._stats5min.length >= 2 ? this._stats5min : this._statsHour; 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[] = []; const rows: TemplateResult[] = [];
for (let i = 1; i < data.length; i++) { for (let i = 1; i < data.length; i++) {
const stat = data[i]; const stat = data[i];
@ -192,6 +197,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
} }
private _renderAdjustStat() { private _renderAdjustStat() {
const unit = getDisplayUnit(
this.hass,
this._params!.statistic.statistic_id,
this._params!.statistic
);
return html` return html`
<div class="text-content"> <div class="text-content">
<b>Statistic:</b> ${this._params!.statistic.statistic_id} <b>Statistic:</b> ${this._params!.statistic.statistic_id}
@ -220,9 +230,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
<ha-selector-number <ha-selector-number
label="New Value" label="New Value"
.hass=${this.hass} .hass=${this.hass}
.selector=${this._amountSelector( .selector=${this._amountSelector(unit || undefined)}
this._params!.statistic.display_unit_of_measurement
)}
.value=${this._amount} .value=${this._amount}
.disabled=${this._busy} .disabled=${this._busy}
@value-changed=${(ev) => { @value-changed=${(ev) => {
@ -299,6 +307,11 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
} }
private async _fixIssue(): Promise<void> { private async _fixIssue(): Promise<void> {
const unit = getDisplayUnit(
this.hass,
this._params!.statistic.statistic_id,
this._params!.statistic
);
this._busy = true; this._busy = true;
try { try {
await adjustStatisticsSum( await adjustStatisticsSum(
@ -306,7 +319,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
this._params!.statistic.statistic_id, this._params!.statistic.statistic_id,
this._chosenStat!.start, this._chosenStat!.start,
this._amount! - this._origAmount!, this._amount! - this._origAmount!,
this._params!.statistic.display_unit_of_measurement unit || null
); );
} catch (err: any) { } catch (err: any) {
this._busy = false; this._busy = false;

View File

@ -315,8 +315,11 @@ class HuiEnergyDistrubutionCard
${formatNumber(gasUsage || 0, this.hass.locale, { ${formatNumber(gasUsage || 0, this.hass.locale, {
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
${getEnergyGasUnit(prefs, this._data.statsMetadata) || ${getEnergyGasUnit(
"m³"} this.hass,
prefs,
this._data.statsMetadata
) || "m³"}
</div> </div>
<svg width="80" height="30"> <svg width="80" height="30">
<path d="M40 0 v30" id="gas" /> <path d="M40 0 v30" id="gas" />

View File

@ -274,7 +274,8 @@ export class HuiEnergyGasGraphCard
) as GasSourceTypeEnergyPreference[]; ) as GasSourceTypeEnergyPreference[];
this._unit = this._unit =
getEnergyGasUnit(energyData.prefs, energyData.statsMetadata) || "m³"; getEnergyGasUnit(this.hass, energyData.prefs, energyData.statsMetadata) ||
"m³";
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = []; const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];

View File

@ -130,7 +130,8 @@ export class HuiEnergySourcesTableCard
); );
const gasUnit = const gasUnit =
getEnergyGasUnit(this._data.prefs, this._data.statsMetadata) || ""; getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
"";
const compare = this._data.statsCompare !== undefined; const compare = this._data.statsCompare !== undefined;

View File

@ -205,7 +205,7 @@ export class HuiStatisticsGraphCardEditor
...this._config, ...this._config,
stat_types: configured_stat_types, stat_types: configured_stat_types,
}; };
const displayUnit = this._metaDatas?.[0]?.display_unit_of_measurement; const unitClass = this._metaDatas?.[0]?.unit_class;
return html` return html`
<ha-form <ha-form
@ -223,7 +223,7 @@ export class HuiStatisticsGraphCardEditor
.pickedStatisticLabel=${this.hass!.localize( .pickedStatisticLabel=${this.hass!.localize(
"ui.panel.lovelace.editor.card.statistics-graph.picked_statistic" "ui.panel.lovelace.editor.card.statistics-graph.picked_statistic"
)} )}
.includeDisplayUnitOfMeasurement=${displayUnit} .includeUnitClass=${unitClass}
.ignoreRestrictionsOnFirstStatistic=${true} .ignoreRestrictionsOnFirstStatistic=${true}
.value=${this._configEntities} .value=${this._configEntities}
.configValue=${"entities"} .configValue=${"entities"}

View File

@ -1,13 +1,26 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-tabs/paper-tab"; import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs"; import "@polymer/paper-tabs/paper-tabs";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import {
import { customElement, property, state } from "lit/decorators"; css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { navigate } from "../../../../common/navigate"; import { navigate } from "../../../../common/navigate";
import { deepEqual } from "../../../../common/util/deep-equal";
import "../../../../components/ha-alert";
import "../../../../components/ha-circular-progress"; import "../../../../components/ha-circular-progress";
import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog";
import "../../../../components/ha-alert"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { import type {
LovelaceBadgeConfig, LovelaceBadgeConfig,
LovelaceCardConfig, LovelaceCardConfig,
@ -20,6 +33,11 @@ import {
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../../components/hui-entity-editor"; import "../../components/hui-entity-editor";
import {
DEFAULT_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
VIEWS_NO_BADGE_SUPPORT,
} from "../../views/const";
import { addView, deleteView, replaceView } from "../config-util"; import { addView, deleteView, replaceView } from "../config-util";
import "../hui-badge-preview"; import "../hui-badge-preview";
import { processEditorEntities } from "../process-editor-entities"; import { processEditorEntities } from "../process-editor-entities";
@ -31,12 +49,6 @@ import {
import "./hui-view-editor"; import "./hui-view-editor";
import "./hui-view-visibility-editor"; import "./hui-view-visibility-editor";
import { EditViewDialogParams } from "./show-edit-view-dialog"; import { EditViewDialogParams } from "./show-edit-view-dialog";
import {
DEFAULT_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
VIEWS_NO_BADGE_SUPPORT,
} from "../../views/const";
import { deepEqual } from "../../../../common/util/deep-equal";
@customElement("hui-dialog-edit-view") @customElement("hui-dialog-edit-view")
export class HuiDialogEditView extends LitElement { export class HuiDialogEditView extends LitElement {
@ -56,6 +68,10 @@ export class HuiDialogEditView extends LitElement {
@state() private _dirty = false; @state() private _dirty = false;
@state() private _yamlMode = false;
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
private _curTabIndex = 0; private _curTabIndex = 0;
get _type(): string { get _type(): string {
@ -67,6 +83,16 @@ export class HuiDialogEditView extends LitElement {
: this._config.type || DEFAULT_VIEW_LAYOUT; : this._config.type || DEFAULT_VIEW_LAYOUT;
} }
protected updated(changedProperties: PropertyValues) {
if (this._yamlMode && changedProperties.has("_yamlMode")) {
const viewConfig = {
...this._config,
badges: this._badges,
};
this._editor?.setValue(viewConfig);
}
}
public showDialog(params: EditViewDialogParams): void { public showDialog(params: EditViewDialogParams): void {
this._params = params; this._params = params;
@ -89,6 +115,7 @@ export class HuiDialogEditView extends LitElement {
this._params = undefined; this._params = undefined;
this._config = {}; this._config = {};
this._badges = []; this._badges = [];
this._yamlMode = false;
this._dirty = false; this._dirty = false;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@ -111,62 +138,74 @@ export class HuiDialogEditView extends LitElement {
} }
let content; let content;
switch (this._curTab) {
case "tab-settings": if (this._yamlMode) {
content = html` content = html`
<hui-view-editor <ha-yaml-editor
.isNew=${this._params.viewIndex === undefined} .hass=${this.hass}
.hass=${this.hass} dialogInitialFocus
.config=${this._config} @value-changed=${this._viewYamlChanged}
@view-config-changed=${this._viewConfigChanged} ></ha-yaml-editor>
></hui-view-editor> `;
`; } else {
break; switch (this._curTab) {
case "tab-badges": case "tab-settings":
content = html` content = html`
${this._badges?.length <hui-view-editor
? html` .isNew=${this._params.viewIndex === undefined}
${VIEWS_NO_BADGE_SUPPORT.includes(this._type) .hass=${this.hass}
? html` .config=${this._config}
<ha-alert alert-type="warning"> @view-config-changed=${this._viewConfigChanged}
${this.hass!.localize( ></hui-view-editor>
"ui.panel.lovelace.editor.edit_badges.view_no_badges" `;
)} break;
</ha-alert> case "tab-badges":
` content = html`
: ""} ${this._badges?.length
<div class="preview-badges"> ? html`
${this._badges.map( ${VIEWS_NO_BADGE_SUPPORT.includes(this._type)
(badgeConfig) => html` ? html`
<hui-badge-preview <ha-alert alert-type="warning">
.hass=${this.hass} ${this.hass!.localize(
.config=${badgeConfig} "ui.panel.lovelace.editor.edit_badges.view_no_badges"
></hui-badge-preview> )}
` </ha-alert>
)} `
</div> : ""}
` <div class="preview-badges">
: ""} ${this._badges.map(
<hui-entity-editor (badgeConfig) => html`
.hass=${this.hass} <hui-badge-preview
.entities=${this._badges} .hass=${this.hass}
@entities-changed=${this._badgesChanged} .config=${badgeConfig}
></hui-entity-editor> ></hui-badge-preview>
`; `
break; )}
case "tab-visibility": </div>
content = html` `
<hui-view-visibility-editor : ""}
.hass=${this.hass} <hui-entity-editor
.config=${this._config} .hass=${this.hass}
@view-visibility-changed=${this._viewVisibilityChanged} .entities=${this._badges}
></hui-view-visibility-editor> @entities-changed=${this._badgesChanged}
`; ></hui-entity-editor>
break; `;
case "tab-cards": break;
content = html` Cards `; case "tab-visibility":
break; content = html`
<hui-view-visibility-editor
.hass=${this.hass}
.config=${this._config}
@view-visibility-changed=${this._viewVisibilityChanged}
></hui-view-visibility-editor>
`;
break;
case "tab-cards":
content = html` Cards `;
break;
}
} }
return html` return html`
<ha-dialog <ha-dialog
open open
@ -174,31 +213,75 @@ export class HuiDialogEditView extends LitElement {
escapeKeyAction escapeKeyAction
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${this._viewConfigTitle} .heading=${this._viewConfigTitle}
class=${classMap({
"yaml-mode": this._yamlMode,
})}
> >
<div slot="heading"> <div slot="heading">
<h2>${this._viewConfigTitle}</h2> <h2>${this._viewConfigTitle}</h2>
<paper-tabs <ha-button-menu
scrollable slot="icons"
hide-scroll-buttons fixed
.selected=${this._curTabIndex} corner="BOTTOM_END"
@selected-item-changed=${this._handleTabSelected} menuCorner="END"
@action=${this._handleAction}
@closed=${stopPropagation}
> >
<paper-tab id="tab-settings" dialogInitialFocus <ha-icon-button
>${this.hass!.localize( slot="trigger"
"ui.panel.lovelace.editor.edit_view.tab_settings" .label=${this.hass!.localize("ui.common.menu")}
)}</paper-tab .path=${mdiDotsVertical}
> ></ha-icon-button>
<paper-tab id="tab-badges" <mwc-list-item graphic="icon">
>${this.hass!.localize( ${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_badges" "ui.panel.lovelace.editor.edit_view.edit_ui"
)}</paper-tab )}
> ${!this._yamlMode
<paper-tab id="tab-visibility" ? html`<ha-svg-icon
>${this.hass!.localize( class="selected_menu_item"
"ui.panel.lovelace.editor.edit_view.tab_visibility" slot="graphic"
)}</paper-tab .path=${mdiCheck}
> ></ha-svg-icon>`
</paper-tabs> : ``}
</mwc-list-item>
<mwc-list-item graphic="icon">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.edit_yaml"
)}
${this._yamlMode
? html`<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
: ``}
</mwc-list-item>
</ha-button-menu>
${!this._yamlMode
? html`<paper-tabs
scrollable
hide-scroll-buttons
.selected=${this._curTabIndex}
@selected-item-changed=${this._handleTabSelected}
>
<paper-tab id="tab-settings" dialogInitialFocus
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_settings"
)}</paper-tab
>
<paper-tab id="tab-badges"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_badges"
)}</paper-tab
>
<paper-tab id="tab-visibility"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_visibility"
)}</paper-tab
>
</paper-tabs>`
: ""}
</div> </div>
${content} ${content}
${this._params.viewIndex !== undefined ${this._params.viewIndex !== undefined
@ -235,6 +318,19 @@ export class HuiDialogEditView extends LitElement {
`; `;
} }
private async _handleAction(ev: CustomEvent<ActionDetail>) {
ev.stopPropagation();
ev.preventDefault();
switch (ev.detail.index) {
case 0:
this._yamlMode = false;
break;
case 1:
this._yamlMode = true;
break;
}
}
private async _delete(): Promise<void> { private async _delete(): Promise<void> {
if (!this._params) { if (!this._params) {
return; return;
@ -348,6 +444,17 @@ export class HuiDialogEditView extends LitElement {
this._dirty = true; 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 { private _isConfigChanged(): boolean {
return ( return (
this._creatingView || this._creatingView ||
@ -366,6 +473,9 @@ export class HuiDialogEditView extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-dialog.yaml-mode {
--dialog-content-padding: 0;
}
h2 { h2 {
display: block; display: block;
color: var(--primary-text-color); color: var(--primary-text-color);
@ -421,6 +531,22 @@ export class HuiDialogEditView extends LitElement {
ha-circular-progress[active] { ha-circular-progress[active] {
display: block; 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 { .hidden {
display: none; display: none;
} }

View File

@ -33,7 +33,7 @@ export class HuiViewEditor extends LitElement {
private _suggestedPath = false; private _suggestedPath = false;
private _schema = memoizeOne( private _schema = memoizeOne(
(localize: LocalizeFunc, subview: boolean, showAdvanced: boolean) => (localize: LocalizeFunc) =>
[ [
{ name: "title", selector: { text: {} } }, { name: "title", selector: { text: {} } },
{ {
@ -69,14 +69,6 @@ export class HuiViewEditor extends LitElement {
boolean: {}, boolean: {},
}, },
}, },
...(subview && showAdvanced
? [
{
name: "back_path",
selector: { navigation: {} },
},
]
: []),
] as const ] as const
); );
@ -98,11 +90,7 @@ export class HuiViewEditor extends LitElement {
return html``; return html``;
} }
const schema = this._schema( const schema = this._schema(this.hass.localize);
this.hass.localize,
this._config.subview ?? false,
this.hass.userData?.showAdvanced ?? false
);
const data = { const data = {
theme: "Backend-selected", theme: "Backend-selected",
@ -128,9 +116,6 @@ export class HuiViewEditor extends LitElement {
if (config.type === "masonry") { if (config.type === "masonry") {
delete config.type; delete config.type;
} }
if (!config.subview) {
delete config.back_path;
}
if ( if (
this.isNew && this.isNew &&
@ -155,10 +140,6 @@ export class HuiViewEditor extends LitElement {
return this.hass.localize("ui.panel.lovelace.editor.edit_view.type"); return this.hass.localize("ui.panel.lovelace.editor.edit_view.type");
case "subview": case "subview":
return this.hass.localize("ui.panel.lovelace.editor.edit_view.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: default:
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}` `ui.panel.lovelace.editor.card.generic.${schema.name}`
@ -174,10 +155,6 @@ export class HuiViewEditor extends LitElement {
return this.hass.localize( return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.subview_helper" "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: default:
return undefined; return undefined;
} }

View File

@ -1855,8 +1855,8 @@
"show_info": "Information", "show_info": "Information",
"default_name": "New Automation", "default_name": "New Automation",
"missing_name": "Cannot save automation without a name", "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_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_not_deletable": "Only automations in automations.yaml can be deleted.",
"load_error_unknown": "Error loading automation ({err_no}).", "load_error_unknown": "Error loading automation ({err_no}).",
"save": "Save", "save": "Save",
@ -2341,7 +2341,6 @@
"parallel": "Max number of parallel runs" "parallel": "Max number of parallel runs"
}, },
"load_error_not_editable": "Only scripts inside scripts.yaml are editable.", "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_not_deletable": "Only scripts in scripts.yaml can be deleted.",
"load_error_unknown": "Error loading script ({err_no}).", "load_error_unknown": "Error loading script ({err_no}).",
"delete_confirm_title": "Delete script?", "delete_confirm_title": "Delete script?",
@ -2610,6 +2609,7 @@
}, },
"devices": { "devices": {
"add_prompt": "No {name} have been added using this {type} yet. You can add one by clicking the + button above.", "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", "caption": "Devices",
"description": "Manage configured devices", "description": "Manage configured devices",
"device_info": "{type} info", "device_info": "{type} info",
@ -2985,8 +2985,6 @@
"error": "Error", "error": "Error",
"could_not_load": "Config flow could not be loaded", "could_not_load": "Config flow could not be loaded",
"not_loaded": "The integration could not be loaded, try to restart Home Assistant.", "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?", "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}.", "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", "supported_hardware": "supported hardware",
@ -3059,7 +3057,9 @@
"editor": { "editor": {
"caption": "Add Credential", "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.", "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", "add": "Add",
"domain": "Integration", "domain": "Integration",
"name": "Name", "name": "Name",
@ -3804,8 +3804,8 @@
}, },
"subview": "Subview", "subview": "Subview",
"subview_helper": "Subviews don't appear in tabs and have a back button.", "subview_helper": "Subviews don't appear in tabs and have a back button.",
"back_path": "Back path (optional)", "edit_ui": "Edit in visual editor",
"back_path_helper": "Only for subviews. If empty, clicking on back button will go to the previous page." "edit_yaml": "Edit in YAML"
}, },
"edit_badges": { "edit_badges": {
"view_no_badges": "Badges are not be supported by the current view type." "view_no_badges": "Badges are not be supported by the current view type."

View File

@ -3,6 +3,7 @@ export interface BrandsOptions {
type: "icon" | "logo" | "icon@2x" | "logo@2x"; type: "icon" | "logo" | "icon@2x" | "logo@2x";
useFallback?: boolean; useFallback?: boolean;
darkOptimized?: boolean; darkOptimized?: boolean;
brand?: boolean;
} }
export interface HardwareBrandsOptions { export interface HardwareBrandsOptions {
@ -13,9 +14,11 @@ export interface HardwareBrandsOptions {
} }
export const brandsUrl = (options: BrandsOptions): string => export const brandsUrl = (options: BrandsOptions): string =>
`https://brands.home-assistant.io/${options.useFallback ? "_/" : ""}${ `https://brands.home-assistant.io/${options.brand ? "brands/" : ""}${
options.domain options.useFallback ? "_/" : ""
}/${options.darkOptimized ? "dark_" : ""}${options.type}.png`; }${options.domain}/${options.darkOptimized ? "dark_" : ""}${
options.type
}.png`;
export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string => export const hardwareBrandsUrl = (options: HardwareBrandsOptions): string =>
`https://brands.home-assistant.io/hardware/${options.category}/${ `https://brands.home-assistant.io/hardware/${options.category}/${