Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
932120869b Add checkbox mode to boolean selector 2024-08-26 19:14:31 +02:00
62 changed files with 610 additions and 1607 deletions

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.1.cjs yarnPath: .yarn/releases/yarn-4.4.0.cjs

View File

@@ -33,7 +33,7 @@
"@codemirror/legacy-modes": "6.4.1", "@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.33.0", "@codemirror/view": "6.32.0",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8", "@formatjs/intl-displaynames": "6.6.8",
@@ -258,5 +258,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
}, },
"packageManager": "yarn@4.4.1" "packageManager": "yarn@4.4.0"
} }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240828.0" version = "20240809.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

@@ -71,7 +71,8 @@ export const computeStateDisplayFromEntityAttributes = (
if ( if (
attributes.device_class === "duration" && attributes.device_class === "duration" &&
attributes.unit_of_measurement && attributes.unit_of_measurement &&
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] &&
entity?.display_precision === undefined
) { ) {
try { try {
return formatDuration(state, attributes.unit_of_measurement); return formatDuration(state, attributes.unit_of_measurement);

View File

@@ -1,6 +0,0 @@
import type { ChartEvent } from "chart.js";
export const clickIsTouch = (event: ChartEvent): boolean =>
!(event.native instanceof MouseEvent) ||
(event.native instanceof PointerEvent &&
event.native.pointerType !== "mouse");

View File

@@ -16,7 +16,6 @@ import {
HaChartBase, HaChartBase,
MIN_TIME_BETWEEN_UPDATES, MIN_TIME_BETWEEN_UPDATES,
} from "./ha-chart-base"; } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
const safeParseFloat = (value) => { const safeParseFloat = (value) => {
const parsed = parseFloat(value); const parsed = parseFloat(value);
@@ -221,7 +220,12 @@ export class StateHistoryChartLine extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) { if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent &&
e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@@ -16,7 +16,6 @@ import {
} from "./ha-chart-base"; } from "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const"; import type { TimeLineData } from "./timeline-chart/const";
import { computeTimelineColor } from "./timeline-chart/timeline-color"; import { computeTimelineColor } from "./timeline-chart/timeline-color";
import { clickIsTouch } from "./click_is_touch";
@customElement("state-history-chart-timeline") @customElement("state-history-chart-timeline")
export class StateHistoryChartTimeline extends LitElement { export class StateHistoryChartTimeline extends LitElement {
@@ -225,7 +224,11 @@ export class StateHistoryChartTimeline extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) { if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@@ -39,7 +39,6 @@ import type {
ChartDatasetExtra, ChartDatasetExtra,
HaChartBase, HaChartBase,
} from "./ha-chart-base"; } from "./ha-chart-base";
import { clickIsTouch } from "./click_is_touch";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = { export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean", mean: "mean",
@@ -279,7 +278,11 @@ export class StatisticsChart extends LitElement {
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if (!this.clickForMoreInfo || clickIsTouch(e)) { if (
!this.clickForMoreInfo ||
!(e.native instanceof MouseEvent) ||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
) {
return; return;
} }

View File

@@ -45,35 +45,15 @@ export class HaConversationAgentPicker extends LitElement {
if (!this._agents) { if (!this._agents) {
return nothing; return nothing;
} }
let value = this.value; const value =
if (!value && this.required) { this.value ??
// Select Home Assistant conversation agent if it supports the language (this.required &&
for (const agent of this._agents) { (!this.language ||
if ( this._agents
agent.id === "conversation.home_assistant" && .find((agent) => agent.id === "homeassistant")
agent.supported_languages.includes(this.language!) ?.supported_languages.includes(this.language))
) { ? "homeassistant"
value = agent.id; : NONE);
break;
}
}
if (!value) {
// Select the first agent that supports the language
for (const agent of this._agents) {
if (
agent.supported_languages === "*" &&
agent.supported_languages.includes(this.language!)
) {
value = agent.id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||

View File

@@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
` `
: ""} : ""}
<slot name="icons"></slot>
</div> </div>
<slot name="icons"></slot>
</div> </div>
<div <div
class="container ${classMap({ expanded: this.expanded })}" class="container ${classMap({ expanded: this.expanded })}"

View File

@@ -21,45 +21,13 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
@property({ attribute: false }) public computeLabel?: ( @property({ attribute: false }) public computeLabel?: (
schema: HaFormSchema, schema: HaFormSchema,
data?: HaFormDataContainer, data?: HaFormDataContainer
options?: { path?: string[] }
) => string; ) => string;
@property({ attribute: false }) public computeHelper?: ( @property({ attribute: false }) public computeHelper?: (
schema: HaFormSchema, schema: HaFormSchema
options?: { path?: string[] }
) => string; ) => string;
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
}
private _computeLabel = (
schema: HaFormSchema,
data?: HaFormDataContainer,
options?: { path?: string[] }
) => {
if (!this.computeLabel) return this.computeLabel;
return this.computeLabel(schema, data, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
private _computeHelper = (
schema: HaFormSchema,
options?: { path?: string[] }
) => {
if (!this.computeHelper) return this.computeHelper;
return this.computeHelper(schema, {
...options,
path: [...(options?.path || []), this.schema.name],
});
};
protected render() { protected render() {
return html` return html`
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}> <ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
@@ -75,17 +43,16 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon> <ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
` `
: nothing} : nothing}
${this.schema.title || this.computeLabel?.(this.schema)} ${this.schema.title}
</div> </div>
<div class="content"> <div class="content">
${this._renderDescription()}
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this.data} .data=${this.data}
.schema=${this.schema.schema} .schema=${this.schema.schema}
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this._computeLabel} .computeLabel=${this.computeLabel}
.computeHelper=${this._computeHelper} .computeHelper=${this.computeHelper}
></ha-form> ></ha-form>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>
@@ -104,9 +71,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.content { .content {
padding: 12px; padding: 12px;
} }
.content p {
margin: 0 0 24px;
}
ha-expansion-panel { ha-expansion-panel {
display: block; display: block;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;

View File

@@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
}; };
const getValue = (obj, item) => const getValue = (obj, item) =>
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null; obj ? (!item.name ? obj : obj[item.name]) : null;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null); const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
@@ -73,6 +73,10 @@ export class HaForm extends LitElement implements HaFormElement {
schema: any schema: any
) => string | undefined; ) => string | undefined;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
protected getFormProperties(): Record<string, any> { protected getFormProperties(): Record<string, any> {
return {}; return {};
} }
@@ -145,6 +149,7 @@ export class HaForm extends LitElement implements HaFormElement {
.disabled=${item.disabled || this.disabled || false} .disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default} .placeholder=${item.required ? "" : item.default}
.helper=${this._computeHelper(item)} .helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false} .required=${item.required || false}
.context=${this._generateContext(item)} .context=${this._generateContext(item)}
></ha-selector>` ></ha-selector>`
@@ -199,8 +204,7 @@ export class HaForm extends LitElement implements HaFormElement {
if (ev.target === this) return; if (ev.target === this) return;
const newValue = const newValue = !schema.name
!schema.name || ("flatten" in schema && schema.flatten)
? ev.detail.value ? ev.detail.value
: { [schema.name]: ev.detail.value }; : { [schema.name]: ev.detail.value };

View File

@@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
export interface HaFormGridSchema extends HaFormBaseSchema { export interface HaFormGridSchema extends HaFormBaseSchema {
type: "grid"; type: "grid";
flatten?: boolean; name: string;
column_min_width?: string; column_min_width?: string;
schema: readonly HaFormSchema[]; schema: readonly HaFormSchema[];
} }
export interface HaFormExpandableSchema extends HaFormBaseSchema { export interface HaFormExpandableSchema extends HaFormBaseSchema {
type: "expandable"; type: "expandable";
flatten?: boolean; name: "";
title?: string; title: string;
icon?: string; icon?: string;
iconPath?: string; iconPath?: string;
expanded?: boolean; expanded?: boolean;
@@ -100,7 +100,7 @@ export type SchemaUnion<
SchemaArray extends readonly HaFormSchema[], SchemaArray extends readonly HaFormSchema[],
Schema = SchemaArray[number], Schema = SchemaArray[number],
> = Schema extends HaFormGridSchema | HaFormExpandableSchema > = Schema extends HaFormGridSchema | HaFormExpandableSchema
? SchemaUnion<Schema["schema"]> | Schema ? SchemaUnion<Schema["schema"]>
: Schema; : Schema;
export interface HaFormDataContainer { export interface HaFormDataContainer {

View File

@@ -1,24 +1,24 @@
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button"; import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import { mdiRestore } from "@mdi/js"; import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import {
CardGridSize,
DEFAULT_GRID_SIZE,
} from "../panels/lovelace/common/compute-card-grid-size";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
@customElement("ha-grid-size-picker") @customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement { export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: CardGridSize; @property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 8; @property({ attribute: false }) public rows = 8;
@@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean; @property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 }; @state() public _localValue?: GridSizeValue = undefined;
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
if (changedProperties.has("value")) { if (changedProperties.has("value")) {
@@ -49,7 +49,6 @@ export class HaGridSizeEditor extends LitElement {
this.rowMin !== undefined && this.rowMin === this.rowMax; this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto"; const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
const rowMin = this.rowMin ?? 1; const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows; const rowMax = this.rowMax ?? this.rows;
@@ -68,7 +67,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin} .min=${columnMin}
.max=${columnMax} .max=${columnMax}
.range=${this.columns} .range=${this.columns}
.value=${fullWidth ? this.columns : columnValue} .value=${columnValue}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns} .disabled=${disabledColumns}
@@ -105,12 +104,12 @@ export class HaGridSizeEditor extends LitElement {
` `
: nothing} : nothing}
<div <div
class="preview ${classMap({ "full-width": fullWidth })}" class="preview"
style=${styleMap({ style=${styleMap({
"--total-rows": this.rows, "--total-rows": this.rows,
"--total-columns": this.columns, "--total-columns": this.columns,
"--rows": rowValue, "--rows": rowValue,
"--columns": fullWidth ? this.columns : columnValue, "--columns": columnValue,
})} })}
> >
<div> <div>
@@ -141,21 +140,12 @@ export class HaGridSizeEditor extends LitElement {
const cell = ev.currentTarget as HTMLElement; const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row")); const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column")); const columns = Number(cell.getAttribute("data-column"));
const clampedRow: CardGridSize["rows"] = conditionalClamp( const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
rows, const clampedColumn = conditionalClamp(
this.rowMin,
this.rowMax
);
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
columns, columns,
this.columnMin, this.columnMin,
this.columnMax this.columnMax
); );
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
if (currentSize.columns === "full" && clampedColumn === this.columns) {
clampedColumn = "full";
}
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn }, value: { rows: clampedRow, columns: clampedColumn },
}); });
@@ -163,23 +153,12 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id as "rows" | "columns"; const key = ev.currentTarget.id;
const currentSize = this.value ?? DEFAULT_GRID_SIZE; const newValue = {
let value = ev.detail.value as CardGridSize[typeof key]; ...this.value,
[key]: ev.detail.value,
if (
key === "columns" &&
currentSize.columns === "full" &&
value === this.columns
) {
value = "full";
}
const newSize = {
...currentSize,
[key]: value,
}; };
fireEvent(this, "value-changed", { value: newSize }); fireEvent(this, "value-changed", { value: newValue });
} }
private _reset(ev) { private _reset(ev) {
@@ -194,14 +173,11 @@ export class HaGridSizeEditor extends LitElement {
private _sliderMoved(ev) { private _sliderMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
const key = ev.currentTarget.id as "rows" | "columns"; const key = ev.currentTarget.id;
const currentSize = this.value ?? DEFAULT_GRID_SIZE; const value = ev.detail.value;
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
if (value === undefined) return; if (value === undefined) return;
this._localValue = { this._localValue = {
...currentSize, ...this.value,
[key]: ev.detail.value, [key]: ev.detail.value,
}; };
} }
@@ -213,7 +189,7 @@ export class HaGridSizeEditor extends LitElement {
grid-template-areas: grid-template-areas:
"reset column-slider" "reset column-slider"
"row-slider preview"; "row-slider preview";
grid-template-rows: auto auto; grid-template-rows: auto 1fr;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 8px; gap: 8px;
} }
@@ -229,12 +205,17 @@ export class HaGridSizeEditor extends LitElement {
.preview { .preview {
position: relative; position: relative;
grid-area: preview; grid-area: preview;
aspect-ratio: 1 / 1.2;
} }
.preview > div { .preview > div {
position: relative; position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: grid; display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr); grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 25px); grid-template-rows: repeat(var(--total-rows), 1fr);
gap: 4px; gap: 4px;
} }
.preview .cell { .preview .cell {
@@ -245,23 +226,15 @@ export class HaGridSizeEditor extends LitElement {
opacity: 0.2; opacity: 0.2;
cursor: pointer; cursor: pointer;
} }
.preview .selected { .selected {
position: absolute;
pointer-events: none; pointer-events: none;
top: 0;
left: 0;
height: 100%;
width: 100%;
} }
.selected .cell { .selected .cell {
background-color: var(--primary-color); background-color: var(--primary-color);
grid-column: 1 / span min(var(--columns, 0), var(--total-columns)); grid-column: 1 / span var(--columns, 0);
grid-row: 1 / span min(var(--rows, 0), var(--total-rows)); grid-row: 1 / span var(--rows, 0);
opacity: 0.5; opacity: 0.5;
} }
.preview.full-width .selected .cell {
grid-column: 1 / -1;
}
`, `,
]; ];
} }

View File

@@ -1,15 +1,19 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { BooleanSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-checkbox";
import "../ha-formfield"; import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text"; import "../ha-input-helper-text";
import "../ha-switch";
@customElement("ha-selector-boolean") @customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement { export class HaBooleanSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: BooleanSelector;
@property({ type: Boolean }) public value = false; @property({ type: Boolean }) public value = false;
@property() public placeholder?: any; @property() public placeholder?: any;
@@ -21,13 +25,24 @@ export class HaBooleanSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
const checkbox = this.selector.boolean?.mode === "checkbox";
return html` return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}> <ha-formfield .alignEnd=${!checkbox} spaceBetween .label=${this.label}>
${checkbox
? html`
<ha-checkbox
.checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-checkbox>
`
: html`
<ha-switch <ha-switch
.checked=${this.value ?? this.placeholder === true} .checked=${this.value ?? this.placeholder === true}
@change=${this._handleChange} @change=${this._handleChange}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-switch> ></ha-switch>
`}
</ha-formfield> </ha-formfield>
${this.helper ${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@@ -162,14 +162,8 @@ export class HaLocationSelector extends LitElement {
private _computeLabel = ( private _computeLabel = (
entry: SchemaUnion<ReturnType<typeof this._schema>> entry: SchemaUnion<ReturnType<typeof this._schema>>
): string => { ): string =>
if (entry.name) { this.hass.localize(`ui.components.selectors.location.${entry.name}`);
return this.hass.localize(
`ui.components.selectors.location.${entry.name}`
);
}
return "";
};
static styles = css` static styles = css`
ha-locations-editor { ha-locations-editor {

View File

@@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
clearable clearable
.helper=${this.helper} .helper=${this.helper}
.label=${this.label} .label=${this.label}
.enableSecond=${!this.selector.time?.no_second} enable-second
></ha-time-input> ></ha-time-input>
`; `;
} }

View File

@@ -44,7 +44,6 @@ import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
const attributeFilter = (values: any[], attribute: any) => { const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") { if (typeof attribute === "object") {
@@ -497,18 +496,12 @@ export class HaServiceControl extends LitElement {
) || ) ||
dataField.name || dataField.name ||
dataField.key} dataField.key}
.secondary=${this._getSectionDescription( >
${this._renderSectionDescription(
dataField, dataField,
domain, domain,
serviceName serviceName
)} )}
>
<ha-service-section-icon
slot="icons"
.hass=${this.hass}
.service=${this._value!.action}
.section=${dataField.key}
></ha-service-section-icon>
${Object.entries(dataField.fields).map(([key, field]) => ${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField( this._renderField(
{ key, ...field }, { key, ...field },
@@ -529,14 +522,20 @@ export class HaServiceControl extends LitElement {
)} `; )} `;
} }
private _getSectionDescription( private _renderSectionDescription(
dataField: ExtHassService["fields"][number], dataField: ExtHassService["fields"][number],
domain: string | undefined, domain: string | undefined,
serviceName: string | undefined serviceName: string | undefined
) { ) {
return this.hass!.localize( const description = this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description` `component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
); );
if (!description) {
return nothing;
}
return html`<p>${description}</p>`;
} }
private _renderField = ( private _renderField = (

View File

@@ -1,53 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
import { serviceSectionIcon } from "../data/icons";
@customElement("ha-service-section-icon")
export class HaServiceSectionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public service?: string;
@property() public section?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.service || !this.section) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
(icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
}
);
return html`${until(icon)}`;
}
private _renderFallback() {
return nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-service-section-icon": HaServiceSectionIcon;
}
}

View File

@@ -16,10 +16,11 @@ import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" };
@customElement("ha-stt-picker") @customElement("ha-stt-picker")
export class HaSTTPicker extends LitElement { export class HaSTTPicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@@ -40,32 +41,13 @@ export class HaSTTPicker extends LitElement {
if (!this._engines) { if (!this._engines) {
return nothing; return nothing;
} }
const value =
let value = this.value; this.value ??
if (!value && this.required) { (this.required
for (const entity of Object.values(this.hass.entities)) { ? this._engines.find(
if ( (engine) => engine.supported_languages?.length !== 0
entity.platform === "cloud" && )
computeDomain(entity.entity_id) === "stt" : NONE);
) {
value = entity.entity_id;
break;
}
}
if (!value) {
for (const sttEngine of this._engines) {
if (sttEngine?.supported_languages?.length !== 0) {
value = sttEngine.engine_id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||
@@ -84,15 +66,12 @@ export class HaSTTPicker extends LitElement {
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
${this._engines.map((engine) => { ${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) { let label = engine.engine_id;
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) { if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id]; const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id; label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else { } else if (engine.engine_id in NAME_MAP) {
label = engine.name || engine.engine_id; label = NAME_MAP[engine.engine_id];
} }
return html`<ha-list-item return html`<ha-list-item
.value=${engine.engine_id} .value=${engine.engine_id}

View File

@@ -16,10 +16,14 @@ import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
const NAME_MAP = {
cloud: "Home Assistant Cloud",
google_translate: "Google Translate",
};
@customElement("ha-tts-picker") @customElement("ha-tts-picker")
export class HaTTSPicker extends LitElement { export class HaTTSPicker extends LitElement {
@property() public value?: string; @property() public value?: string;
@@ -40,32 +44,13 @@ export class HaTTSPicker extends LitElement {
if (!this._engines) { if (!this._engines) {
return nothing; return nothing;
} }
const value =
let value = this.value; this.value ??
if (!value && this.required) { (this.required
for (const entity of Object.values(this.hass.entities)) { ? this._engines.find(
if ( (engine) => engine.supported_languages?.length !== 0
entity.platform === "cloud" && )
computeDomain(entity.entity_id) === "tts" : NONE);
) {
value = entity.entity_id;
break;
}
}
if (!value) {
for (const ttsEngine of this._engines) {
if (ttsEngine?.supported_languages?.length !== 0) {
value = ttsEngine.engine_id;
break;
}
}
}
}
if (!value) {
value = NONE;
}
return html` return html`
<ha-select <ha-select
.label=${this.label || .label=${this.label ||
@@ -84,15 +69,12 @@ export class HaTTSPicker extends LitElement {
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
${this._engines.map((engine) => { ${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) { let label = engine.engine_id;
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) { if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id]; const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id; label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else { } else if (engine.engine_id in NAME_MAP) {
label = engine.name || engine.engine_id; label = NAME_MAP[engine.engine_id];
} }
return html`<ha-list-item return html`<ha-list-item
.value=${engine.engine_id} .value=${engine.engine_id}

View File

@@ -11,7 +11,6 @@ import {
isLastDayOfMonth, isLastDayOfMonth,
} from "date-fns"; } from "date-fns";
import { Collection, getCollection } from "home-assistant-js-websocket"; import { Collection, getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { import {
calcDate, calcDate,
calcDateProperty, calcDateProperty,
@@ -792,147 +791,3 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
export const energyStatisticHelpUrl = export const energyStatisticHelpUrl =
"/docs/energy/faq/#troubleshooting-missing-entities"; "/docs/energy/faq/#troubleshooting-missing-entities";
interface EnergySumData {
to_grid?: { [start: number]: number };
from_grid?: { [start: number]: number };
to_battery?: { [start: number]: number };
from_battery?: { [start: number]: number };
solar?: { [start: number]: number };
}
interface EnergyConsumptionData {
total: { [start: number]: number };
}
export const getSummedData = memoizeOne(
(
data: EnergyData
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
const summedData = getSummedDataPartial(data);
const compareSummedData = data.statsCompare
? getSummedDataPartial(data, true)
: undefined;
return { summedData, compareSummedData };
}
);
const getSummedDataPartial = (
data: EnergyData,
compare?: boolean
): EnergySumData => {
const statIds: {
to_grid?: string[];
from_grid?: string[];
solar?: string[];
to_battery?: string[];
from_battery?: string[];
} = {};
for (const source of data.prefs.energy_sources) {
if (source.type === "solar") {
if (statIds.solar) {
statIds.solar.push(source.stat_energy_from);
} else {
statIds.solar = [source.stat_energy_from];
}
continue;
}
if (source.type === "battery") {
if (statIds.to_battery) {
statIds.to_battery.push(source.stat_energy_to);
statIds.from_battery!.push(source.stat_energy_from);
} else {
statIds.to_battery = [source.stat_energy_to];
statIds.from_battery = [source.stat_energy_from];
}
continue;
}
if (source.type !== "grid") {
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
if (statIds.from_grid) {
statIds.from_grid.push(flowFrom.stat_energy_from);
} else {
statIds.from_grid = [flowFrom.stat_energy_from];
}
}
for (const flowTo of source.flow_to) {
if (statIds.to_grid) {
statIds.to_grid.push(flowTo.stat_energy_to);
} else {
statIds.to_grid = [flowTo.stat_energy_to];
}
}
}
const summedData: EnergySumData = {};
Object.entries(statIds).forEach(([key, subStatIds]) => {
const totalStats: { [start: number]: number } = {};
const sets: { [statId: string]: { [start: number]: number } } = {};
subStatIds!.forEach((id) => {
const stats = compare ? data.statsCompare[id] : data.stats[id];
if (!stats) {
return;
}
const set = {};
stats.forEach((stat) => {
if (stat.change === null || stat.change === undefined) {
return;
}
const val = stat.change;
// Get total of solar and to grid to calculate the solar energy used
totalStats[stat.start] =
stat.start in totalStats ? totalStats[stat.start] + val : val;
});
sets[id] = set;
});
summedData[key] = totalStats;
});
return summedData;
};
export const computeConsumptionData = memoizeOne(
(
data: EnergySumData,
compareData?: EnergySumData
): {
consumption: EnergyConsumptionData;
compareConsumption?: EnergyConsumptionData;
} => {
const consumption = computeConsumptionDataPartial(data);
const compareConsumption = compareData
? computeConsumptionDataPartial(compareData)
: undefined;
return { consumption, compareConsumption };
}
);
const computeConsumptionDataPartial = (
data: EnergySumData
): EnergyConsumptionData => {
const outData: EnergyConsumptionData = { total: {} };
Object.keys(data).forEach((type) => {
Object.keys(data[type]).forEach((start) => {
if (outData.total[start] === undefined) {
const consumption =
(data.from_grid?.[start] || 0) +
(data.solar?.[start] || 0) +
(data.from_battery?.[start] || 0) -
(data.to_grid?.[start] || 0) -
(data.to_battery?.[start] || 0);
outData.total[start] = consumption;
}
});
});
return outData;
};

View File

@@ -62,7 +62,7 @@ export interface ComponentIcons {
} }
interface ServiceIcons { interface ServiceIcons {
[service: string]: { service: string; sections?: { [name: string]: string } }; [service: string]: string;
} }
export type IconCategory = "entity" | "entity_component" | "services"; export type IconCategory = "entity" | "entity_component" | "services";
@@ -288,8 +288,7 @@ export const serviceIcon = async (
const serviceName = computeObjectId(service); const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain); const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) { if (serviceIcons) {
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string]; icon = serviceIcons[serviceName] as string;
icon = srvceIcon?.service;
} }
if (!icon) { if (!icon) {
icon = await domainIcon(hass, domain); icon = await domainIcon(hass, domain);
@@ -297,21 +296,6 @@ export const serviceIcon = async (
return icon; return icon;
}; };
export const serviceSectionIcon = async (
hass: HomeAssistant,
service: string,
section: string
): Promise<string | undefined> => {
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) {
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
return srvceIcon?.sections?.[section];
}
return undefined;
};
export const domainIcon = async ( export const domainIcon = async (
hass: HomeAssistant, hass: HomeAssistant,
domain: string, domain: string,

View File

@@ -13,7 +13,7 @@ export const ensureBadgeConfig = (
return { return {
type: "entity", type: "entity",
entity: config, entity: config,
show_name: true, display_type: "complete",
}; };
} }
if ("type" in config && config.type) { if ("type" in config && config.type) {

View File

@@ -5,7 +5,6 @@ import type { LovelaceStrategyConfig } from "./strategy";
export interface LovelaceBaseSectionConfig { export interface LovelaceBaseSectionConfig {
title?: string; title?: string;
visibility?: Condition[]; visibility?: Condition[];
column_span?: number;
} }
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig { export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {

View File

@@ -101,8 +101,9 @@ export interface AttributeSelector {
} }
export interface BooleanSelector { export interface BooleanSelector {
// eslint-disable-next-line @typescript-eslint/ban-types boolean: {
boolean: {} | null; mode?: "checkbox" | "switch";
} | null;
} }
export interface ColorRGBSelector { export interface ColorRGBSelector {
@@ -427,7 +428,8 @@ export interface ThemeSelector {
theme: { include_default?: boolean } | null; theme: { include_default?: boolean } | null;
} }
export interface TimeSelector { export interface TimeSelector {
time: { no_second?: boolean } | null; // eslint-disable-next-line @typescript-eslint/ban-types
time: {} | null;
} }
export interface TriggerSelector { export interface TriggerSelector {

View File

@@ -21,8 +21,6 @@ export interface SpeechMetadata {
export interface STTEngine { export interface STTEngine {
engine_id: string; engine_id: string;
supported_languages?: string[]; supported_languages?: string[];
name?: string;
deprecated: boolean;
} }
export const listSTTEngines = ( export const listSTTEngines = (

View File

@@ -3,8 +3,6 @@ import { HomeAssistant } from "../types";
export interface TTSEngine { export interface TTSEngine {
engine_id: string; engine_id: string;
supported_languages?: string[]; supported_languages?: string[];
name?: string;
deprecated: boolean;
} }
export interface TTSVoice { export interface TTSVoice {

View File

@@ -76,36 +76,17 @@ export const showConfigFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field, options) { renderShowFormStepFieldLabel(hass, step, field) {
if (field.type === "expandable") {
return hass.localize( return hass.localize(
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name` `component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}` : "";
return (
hass.localize(
`component.${step.handler}.config.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
); );
}, },
renderShowFormStepFieldHelper(hass, step, field, options) { renderShowFormStepFieldHelper(hass, step, field) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize( const description = hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${prefix}data_description.${field.name}`, `component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description
? html`<ha-markdown breaks .content=${description}></ha-markdown>` ? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: ""; : "";

View File

@@ -49,15 +49,13 @@ export interface FlowConfig {
renderShowFormStepFieldLabel( renderShowFormStepFieldLabel(
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepForm, step: DataEntryFlowStepForm,
field: HaFormSchema, field: HaFormSchema
options: { path?: string[]; [key: string]: any }
): string; ): string;
renderShowFormStepFieldHelper( renderShowFormStepFieldHelper(
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepForm, step: DataEntryFlowStepForm,
field: HaFormSchema, field: HaFormSchema
options: { path?: string[]; [key: string]: any }
): TemplateResult | string; ): TemplateResult | string;
renderShowFormStepFieldError( renderShowFormStepFieldError(

View File

@@ -93,33 +93,15 @@ export const showOptionsFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field, options) { renderShowFormStepFieldLabel(hass, step, field) {
if (field.type === "expandable") {
return hass.localize( return hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name` `component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
return (
hass.localize(
`component.${configEntry.domain}.options.step.${step.step_id}.${prefix}data.${field.name}`
) || field.name
); );
}, },
renderShowFormStepFieldHelper(hass, step, field, options) { renderShowFormStepFieldHelper(hass, step, field) {
if (field.type === "expandable") {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.description`
);
}
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
const description = hass.localize( const description = hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${prefix}data_description.${field.name}`, `component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description

View File

@@ -225,24 +225,11 @@ class StepFlowForm extends LitElement {
this._stepData = ev.detail.value; this._stepData = ev.detail.value;
} }
private _labelCallback = (field: HaFormSchema, _data, options): string => private _labelCallback = (field: HaFormSchema): string =>
this.flowConfig.renderShowFormStepFieldLabel( this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
this.hass,
this.step,
field,
options
);
private _helperCallback = ( private _helperCallback = (field: HaFormSchema): string | TemplateResult =>
field: HaFormSchema, this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
options
): string | TemplateResult =>
this.flowConfig.renderShowFormStepFieldHelper(
this.hass,
this.step,
field,
options
);
private _errorCallback = (error: string) => private _errorCallback = (error: string) =>
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error); this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);

View File

@@ -51,7 +51,6 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../types"; import { HomeAssistant, Route } from "../../types";
import { subscribeLabelRegistry } from "../../data/label_registry"; import { subscribeLabelRegistry } from "../../data/label_registry";
import { subscribeFloorRegistry } from "../../data/floor_registry"; import { subscribeFloorRegistry } from "../../data/floor_registry";
import { throttle } from "../../common/util/throttle";
declare global { declare global {
// for fire event // for fire event
@@ -396,10 +395,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
initialValue: [], initialValue: [],
}); });
private _hassThrottler = throttle((el, hass) => {
el.hass = hass;
}, 1000);
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
@@ -646,11 +641,7 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
this.hass.dockedSidebar === "docked" ? this._wideSidebar : this._wide; this.hass.dockedSidebar === "docked" ? this._wideSidebar : this._wide;
el.route = this.routeTail; el.route = this.routeTail;
if (el.hass !== undefined) {
this._hassThrottler(el, this.hass);
} else {
el.hass = this.hass; el.hass = this.hass;
}
el.showAdvanced = Boolean(this.hass.userData?.showAdvanced); el.showAdvanced = Boolean(this.hass.userData?.showAdvanced);
el.isWide = isWide; el.isWide = isWide;
el.narrow = this.narrow; el.narrow = this.narrow;

View File

@@ -1,133 +0,0 @@
import { CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-button";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import {
ScheduleBlockInfo,
ScheduleBlockInfoDialogParams,
} from "./show-dialog-schedule-block-info";
import type { SchemaUnion } from "../../../../components/ha-form/types";
const SCHEMA = [
{
name: "from",
required: true,
selector: { time: { no_second: true } },
},
{
name: "to",
required: true,
selector: { time: { no_second: true } },
},
];
class DialogScheduleBlockInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _error?: Record<string, string>;
@state() private _data?: ScheduleBlockInfo;
@state() private _params?: ScheduleBlockInfoDialogParams;
public showDialog(params: ScheduleBlockInfoDialogParams): void {
this._params = params;
this._error = undefined;
this._data = params.block;
}
public closeDialog(): void {
this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params || !this._data) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
)
)}
>
<div>
<ha-form
.hass=${this.hass}
.schema=${SCHEMA}
.data=${this._data}
.error=${this._error}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
<ha-button
slot="secondaryAction"
class="warning"
@click=${this._deleteBlock}
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _valueChanged(ev: CustomEvent) {
this._error = undefined;
this._data = ev.detail.value;
}
private _updateBlock() {
try {
this._params!.updateBlock!(this._data!);
this.closeDialog();
} catch (err: any) {
this._error = { base: err ? err.message : "Unknown error" };
}
}
private _deleteBlock() {
try {
this._params!.deleteBlock!();
this.closeDialog();
} catch (err: any) {
this._error = { base: err ? err.message : "Unknown error" };
}
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "from":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.start");
case "to":
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
}
return "";
};
static get styles(): CSSResultGroup {
return [haStyleDialog];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-schedule-block-info": DialogScheduleBlockInfo;
}
}
customElements.define("dialog-schedule-block-info", DialogScheduleBlockInfo);

View File

@@ -20,7 +20,7 @@ import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule"; import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation"; import { TimeZone } from "../../../../data/translation";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -352,34 +352,21 @@ class HaScheduleForm extends LitElement {
} }
private async _handleEventClick(info: any) { private async _handleEventClick(info: any) {
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.dialogs.helper_settings.schedule.delete"),
text: this.hass.localize(
"ui.dialogs.helper_settings.schedule.confirm_delete"
),
destructive: true,
confirmText: this.hass.localize("ui.common.delete"),
}))
) {
return;
}
const [day, index] = info.event.id.split("-"); const [day, index] = info.event.id.split("-");
const item = [...this[`_${day}`]][index];
showScheduleBlockInfoDialog(this, {
block: item,
updateBlock: (newBlock) => this._updateBlock(day, index, newBlock),
deleteBlock: () => this._deleteBlock(day, index),
});
}
private _updateBlock(day, index, newBlock) {
const [fromH, fromM, _fromS] = newBlock.from.split(":");
newBlock.from = `${fromH}:${fromM}`;
const [toH, toM, _toS] = newBlock.to.split(":");
newBlock.to = `${toH}:${toM}`;
if (Number(toH) === 0 && Number(toM) === 0) {
newBlock.to = "24:00";
}
const newValue = { ...this._item };
newValue[day] = [...this._item![day]];
newValue[day][index] = newBlock;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _deleteBlock(day, index) {
const value = [...this[`_${day}`]]; const value = [...this[`_${day}`]];
const newValue = { ...this._item }; const newValue = { ...this._item };
value.splice(parseInt(index), 1); value.splice(parseInt(index), 1);
newValue[day] = value; newValue[day] = value;

View File

@@ -1,26 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export interface ScheduleBlockInfo {
from: string;
to: string;
}
export interface ScheduleBlockInfoDialogParams {
block: ScheduleBlockInfo;
updateBlock?: (update: ScheduleBlockInfo) => void;
deleteBlock?: () => void;
}
export const loadScheduleBlockInfoDialog = () =>
import("./dialog-schedule-block-info");
export const showScheduleBlockInfoDialog = (
element: HTMLElement,
params: ScheduleBlockInfoDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-schedule-block-info",
dialogImport: loadScheduleBlockInfoDialog,
dialogParams: params,
});
};

View File

@@ -8,7 +8,6 @@ import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"
import { createCloseHeading } from "../../../../../components/ha-dialog"; import { createCloseHeading } from "../../../../../components/ha-dialog";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-select"; import "../../../../../components/ha-select";
import "../../../../../components/ha-list-item"; import "../../../../../components/ha-list-item";
@@ -71,22 +70,10 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
this.hass.localize("ui.panel.config.zha.change_channel_dialog.title") this.hass.localize("ui.panel.config.zha.change_channel_dialog.title")
)} )}
> >
<ha-alert alert-type="warning"> <p>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.migration_warning" "ui.panel.config.zha.change_channel_dialog.migration_warning"
)} )}
</ha-alert>
<p>
${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.description"
)}
</p>
<p>
${this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.smart_explanation"
)}
</p> </p>
<p> <p>
@@ -103,11 +90,7 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
${VALID_CHANNELS.map( ${VALID_CHANNELS.map(
(newChannel) => (newChannel) =>
html`<ha-list-item .value=${String(newChannel)} html`<ha-list-item .value=${String(newChannel)}
>${newChannel === "auto" >${newChannel}</ha-list-item
? this.hass.localize(
"ui.panel.config.zha.change_channel_dialog.channel_auto"
)
: newChannel}</ha-list-item
>` >`
)} )}
</ha-select> </ha-select>

View File

@@ -96,20 +96,20 @@ export const showRepairsFlowDialog = (
: ""; : "";
}, },
renderShowFormStepFieldLabel(hass, step, field, options) { renderShowFormStepFieldLabel(hass, step, field) {
return hass.localize( return hass.localize(
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data.${field.name}`, }.fix_flow.step.${step.step_id}.data.${field.name}`,
step.description_placeholders step.description_placeholders
); );
}, },
renderShowFormStepFieldHelper(hass, step, field, options) { renderShowFormStepFieldHelper(hass, step, field) {
const description = hass.localize( const description = hass.localize(
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data_description.${field.name}`, }.fix_flow.step.${step.step_id}.data_description.${field.name}`,
step.description_placeholders step.description_placeholders
); );
return description return description

View File

@@ -28,7 +28,6 @@ import "./assist-pipeline-detail/assist-pipeline-detail-tts";
import "./assist-pipeline-detail/assist-pipeline-detail-wakeword"; import "./assist-pipeline-detail/assist-pipeline-detail-wakeword";
import "./debug/assist-render-pipeline-events"; import "./debug/assist-render-pipeline-events";
import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail"; import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
@customElement("dialog-voice-assistant-pipeline-detail") @customElement("dialog-voice-assistant-pipeline-detail")
export class DialogVoiceAssistantPipelineDetail extends LitElement { export class DialogVoiceAssistantPipelineDetail extends LitElement {
@@ -55,37 +54,16 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
if (this._params.pipeline) { if (this._params.pipeline) {
this._data = this._params.pipeline; this._data = this._params.pipeline;
this._preferred = this._params.preferred; this._preferred = this._params.preferred;
return; } else {
}
let sstDefault: string | undefined;
let ttsDefault: string | undefined;
if (this._cloudActive) {
for (const entity of Object.values(this.hass.entities)) {
if (entity.platform !== "cloud") {
continue;
}
if (computeDomain(entity.entity_id) === "stt") {
sstDefault = entity.entity_id;
if (ttsDefault) {
break;
}
} else if (computeDomain(entity.entity_id) === "tts") {
ttsDefault = entity.entity_id;
if (sstDefault) {
break;
}
}
}
}
this._data = { this._data = {
language: ( language: (
this.hass.config.language || this.hass.locale.language this.hass.config.language || this.hass.locale.language
).substring(0, 2), ).substring(0, 2),
stt_engine: sstDefault, stt_engine: this._cloudActive ? "cloud" : undefined,
tts_engine: ttsDefault, tts_engine: this._cloudActive ? "cloud" : undefined,
}; };
} }
}
public closeDialog(): void { public closeDialog(): void {
this._params = undefined; this._params = undefined;

View File

@@ -1,4 +1,3 @@
import { mdiAlertCircle } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -6,16 +5,15 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { mdiAlertCircle } from "@mdi/js";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-ripple"; import "../../../components/ha-ripple";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
@@ -24,38 +22,15 @@ import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { LovelaceBadge, LovelaceBadgeEditor } from "../types"; import { LovelaceBadge, LovelaceBadgeEditor } from "../types";
import { EntityBadgeConfig } from "./types"; import { EntityBadgeConfig } from "./types";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const; export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const;
export type DisplayType = (typeof DISPLAY_TYPES)[number]; export type DisplayType = (typeof DISPLAY_TYPES)[number];
export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard"; export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard";
export const DEFAULT_CONFIG: EntityBadgeConfig = {
type: "entity",
show_name: false,
show_state: true,
show_icon: true,
};
export const migrateLegacyEntityBadgeConfig = (
config: EntityBadgeConfig
): EntityBadgeConfig => {
const newConfig = { ...config };
if (config.display_type) {
if (config.show_name === undefined) {
if (config.display_type === "complete") {
newConfig.show_name = true;
}
}
if (config.show_state === undefined) {
if (config.display_type === "minimal") {
newConfig.show_state = false;
}
}
delete newConfig.display_type;
}
return newConfig;
};
@customElement("hui-entity-badge") @customElement("hui-entity-badge")
export class HuiEntityBadge extends LitElement implements LovelaceBadge { export class HuiEntityBadge extends LitElement implements LovelaceBadge {
public static async getConfigElement(): Promise<LovelaceBadgeEditor> { public static async getConfigElement(): Promise<LovelaceBadgeEditor> {
@@ -89,10 +64,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
@state() protected _config?: EntityBadgeConfig; @state() protected _config?: EntityBadgeConfig;
public setConfig(config: EntityBadgeConfig): void { public setConfig(config: EntityBadgeConfig): void {
this._config = { this._config = config;
...DEFAULT_CONFIG,
...migrateLegacyEntityBadgeConfig(config),
};
} }
get hasAction() { get hasAction() {
@@ -162,9 +134,9 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
return html` return html`
<div class="badge error"> <div class="badge error">
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon> <ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
<span class="info">
<span class="label">${entityId}</span>
<span class="content"> <span class="content">
<span class="name">${entityId}</span>
<span class="state">
${this.hass.localize("ui.badge.entity.not_found")} ${this.hass.localize("ui.badge.entity.not_found")}
</span> </span>
</span> </span>
@@ -191,25 +163,18 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
const name = this._config.name || stateObj.attributes.friendly_name; const name = this._config.name || stateObj.attributes.friendly_name;
const showState = this._config.show_state; const displayType = this._config.display_type || DEFAULT_DISPLAY_TYPE;
const showName = this._config.show_name;
const showIcon = this._config.show_icon;
const showEntityPicture = this._config.show_entity_picture;
const imageUrl = showEntityPicture const imageUrl = this._config.show_entity_picture
? this._getImageUrl(stateObj) ? this._getImageUrl(stateObj)
: undefined; : undefined;
const label = showState && showName ? name : undefined;
const content = showState ? stateDisplay : showName ? name : undefined;
return html` return html`
<div <div
style=${styleMap(style)} style=${styleMap(style)}
class="badge ${classMap({ class="badge ${classMap({
active, active,
"no-info": !showState && !showName, [displayType]: true,
"no-icon": !showIcon,
})}" })}"
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
@@ -220,8 +185,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
tabindex=${ifDefined(this.hasAction ? "0" : undefined)} tabindex=${ifDefined(this.hasAction ? "0" : undefined)}
> >
<ha-ripple .disabled=${!this.hasAction}></ha-ripple> <ha-ripple .disabled=${!this.hasAction}></ha-ripple>
${showIcon ${imageUrl
? imageUrl
? html`<img src=${imageUrl} aria-hidden />` ? html`<img src=${imageUrl} aria-hidden />`
: html` : html`
<ha-state-icon <ha-state-icon
@@ -229,13 +193,14 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
.stateObj=${stateObj} .stateObj=${stateObj}
.icon=${this._config.icon} .icon=${this._config.icon}
></ha-state-icon> ></ha-state-icon>
` `}
: nothing} ${displayType !== "minimal"
${content
? html` ? html`
<span class="info"> <span class="content">
${label ? html`<span class="label">${name}</span>` : nothing} ${displayType === "complete"
<span class="content">${content}</span> ? html`<span class="name">${name}</span>`
: nothing}
<span class="state">${stateDisplay}</span>
</span> </span>
` `
: nothing} : nothing}
@@ -269,15 +234,12 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
height: var(--ha-badge-size, 36px); height: 36px;
min-width: var(--ha-badge-size, 36px); min-width: 36px;
padding: 0px 8px; padding: 0px 8px;
box-sizing: border-box; box-sizing: border-box;
width: auto; width: auto;
border-radius: var( border-radius: 18px;
--ha-badge-border-radius,
calc(var(--ha-badge-size, 36px) / 2)
);
background: var( background: var(
--ha-card-background, --ha-card-background,
var(--card-background-color, white) var(--card-background-color, white)
@@ -312,7 +274,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
.badge.active { .badge.active {
--badge-color: var(--primary-color); --badge-color: var(--primary-color);
} }
.info { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
@@ -320,7 +282,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
padding-inline-end: 4px; padding-inline-end: 4px;
padding-inline-start: initial; padding-inline-start: initial;
} }
.label { .name {
font-size: 10px; font-size: 10px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@@ -328,7 +290,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
letter-spacing: 0.1px; letter-spacing: 0.1px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.content { .state {
font-size: 12px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
@@ -348,20 +310,14 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
object-fit: cover; object-fit: cover;
overflow: hidden; overflow: hidden;
} }
.badge.no-info { .badge.minimal {
padding: 0; padding: 0;
} }
.badge:not(.no-icon) img { .badge:not(.minimal) img {
margin-left: -6px; margin-left: -6px;
margin-inline-start: -6px; margin-inline-start: -6px;
margin-inline-end: initial; margin-inline-end: initial;
} }
.badge.no-icon .info {
padding-right: 4px;
padding-left: 4px;
padding-inline-end: 4px;
padding-inline-start: 4px;
}
`; `;
} }
} }

View File

@@ -16,10 +16,10 @@ export class HuiStateLabelBadge extends HuiEntityBadge {
const entityBadgeConfig: EntityBadgeConfig = { const entityBadgeConfig: EntityBadgeConfig = {
type: "entity", type: "entity",
entity: config.entity, entity: config.entity,
show_name: config.show_name ?? true, display_type: config.show_name === false ? "standard" : "complete",
}; };
super.setConfig(entityBadgeConfig); this._config = entityBadgeConfig;
} }
} }

View File

@@ -3,7 +3,6 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LegacyStateFilter } from "../common/evaluate-filter"; import type { LegacyStateFilter } from "../common/evaluate-filter";
import type { Condition } from "../common/validate-condition"; import type { Condition } from "../common/validate-condition";
import type { EntityFilterEntityConfig } from "../entity-rows/types"; import type { EntityFilterEntityConfig } from "../entity-rows/types";
import type { DisplayType } from "./hui-entity-badge";
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig { export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
type: "entity-filter"; type: "entity-filter";
@@ -34,16 +33,10 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig {
name?: string; name?: string;
icon?: string; icon?: string;
color?: string; color?: string;
show_name?: boolean;
show_state?: boolean;
show_icon?: boolean;
show_entity_picture?: boolean; show_entity_picture?: boolean;
display_type?: "minimal" | "standard" | "complete";
state_content?: string | string[]; state_content?: string | string[];
tap_action?: ActionConfig; tap_action?: ActionConfig;
hold_action?: ActionConfig; hold_action?: ActionConfig;
double_tap_action?: ActionConfig; double_tap_action?: ActionConfig;
/**
* @deprecated use `show_state`, `show_name`, `icon_type`
*/
display_type?: DisplayType;
} }

View File

@@ -18,22 +18,18 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../../../common/color/colors"; import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color";
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base"; import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import { import {
DeviceConsumptionEnergyPreference, DeviceConsumptionEnergyPreference,
EnergyData, EnergyData,
getEnergyDataCollection, getEnergyDataCollection,
getSummedData,
computeConsumptionData,
} from "../../../../data/energy"; } from "../../../../data/energy";
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
getStatisticLabel, getStatisticLabel,
Statistics, Statistics,
StatisticsMetaData, StatisticsMetaData,
isExternalStatistic,
} from "../../../../data/recorder"; } from "../../../../data/recorder";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@@ -42,9 +38,7 @@ import { LovelaceCard } from "../../types";
import { EnergyDevicesDetailGraphCardConfig } from "../types"; import { EnergyDevicesDetailGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions } from "./common/energy-chart-options"; import { getCommonOptions } from "./common/energy-chart-options";
import { fireEvent } from "../../../../common/dom/fire_event";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { clickIsTouch } from "../../../../components/chart/click_is_touch";
const UNIT = "kWh"; const UNIT = "kWh";
@@ -78,8 +72,6 @@ export class HuiEnergyDevicesDetailGraphCard
}) })
private _hiddenStats: string[] = []; private _hiddenStats: string[] = [];
private _untrackedIndex?: number;
protected hassSubscribeRequiredHostProps = ["_config"]; protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
@@ -157,22 +149,17 @@ export class HuiEnergyDevicesDetailGraphCard
} }
private _datasetHidden(ev) { private _datasetHidden(ev) {
const hiddenEntity = this._hiddenStats = [
ev.detail.index === this._untrackedIndex ...this._hiddenStats,
? "untracked" this._data!.prefs.device_consumption[ev.detail.index].stat_consumption,
: this._data!.prefs.device_consumption[ev.detail.index] ];
.stat_consumption;
this._hiddenStats = [...this._hiddenStats, hiddenEntity];
} }
private _datasetUnhidden(ev) { private _datasetUnhidden(ev) {
const hiddenEntity =
ev.detail.index === this._untrackedIndex
? "untracked"
: this._data!.prefs.device_consumption[ev.detail.index]
.stat_consumption;
this._hiddenStats = this._hiddenStats.filter( this._hiddenStats = this._hiddenStats.filter(
(stat) => stat !== hiddenEntity (stat) =>
stat !==
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
); );
} }
@@ -210,20 +197,6 @@ export class HuiEnergyDevicesDetailGraphCard
}, },
}, },
}, },
onClick: (event, elements, chart) => {
if (clickIsTouch(event)) return;
const index = elements[0]?.datasetIndex ?? -1;
if (index < 0) return;
const statisticId =
this._data?.prefs.device_consumption[index]?.stat_consumption;
if (!statisticId || isExternalStatistic(statisticId)) return;
fireEvent(this, "hass-more-info", { entityId: statisticId });
chart?.canvas?.dispatchEvent(new Event("mouseout")); // to hide tooltip
},
}; };
return options; return options;
} }
@@ -267,33 +240,6 @@ export class HuiEnergyDevicesDetailGraphCard
datasetExtras.push(...processedDataExtras); datasetExtras.push(...processedDataExtras);
const { summedData, compareSummedData } = getSummedData(energyData);
const showUntracked =
"from_grid" in summedData ||
"solar" in summedData ||
"from_battery" in summedData;
const {
consumption: consumptionData,
compareConsumption: consumptionCompareData,
} = showUntracked
? computeConsumptionData(summedData, compareSummedData)
: { consumption: undefined, compareConsumption: undefined };
if (showUntracked) {
this._untrackedIndex = datasets.length;
const { dataset: untrackedData, datasetExtra: untrackedDataExtra } =
this._processUntracked(
computedStyle,
processedData,
consumptionData,
false
);
datasets.push(untrackedData);
datasetExtras.push(untrackedDataExtra);
}
if (compareData) { if (compareData) {
// Add empty dataset to align the bars // Add empty dataset to align the bars
datasets.push({ datasets.push({
@@ -326,20 +272,6 @@ export class HuiEnergyDevicesDetailGraphCard
datasets.push(...processedCompareData); datasets.push(...processedCompareData);
datasetExtras.push(...processedCompareDataExtras); datasetExtras.push(...processedCompareDataExtras);
if (showUntracked) {
const {
dataset: untrackedCompareData,
datasetExtra: untrackedCompareDataExtra,
} = this._processUntracked(
computedStyle,
processedCompareData,
consumptionCompareData,
true
);
datasets.push(untrackedCompareData);
datasetExtras.push(untrackedCompareDataExtra);
}
} }
this._start = energyData.start; this._start = energyData.start;
@@ -354,57 +286,6 @@ export class HuiEnergyDevicesDetailGraphCard
this._chartDatasetExtra = datasetExtras; this._chartDatasetExtra = datasetExtras;
} }
private _processUntracked(
computedStyle: CSSStyleDeclaration,
processedData,
consumptionData,
compare: boolean
): { dataset; datasetExtra } {
const totalDeviceConsumption: { [start: number]: number } = {};
processedData.forEach((device) => {
device.data.forEach((datapoint) => {
totalDeviceConsumption[datapoint.x] =
(totalDeviceConsumption[datapoint.x] || 0) + datapoint.y;
});
});
const untrackedConsumption: { x: number; y: number }[] = [];
Object.keys(consumptionData.total).forEach((time) => {
untrackedConsumption.push({
x: Number(time),
y: consumptionData.total[time] - (totalDeviceConsumption[time] || 0),
});
});
const dataset = {
label: this.hass.localize("ui.panel.energy.charts.untracked_consumption"),
hidden: this._hiddenStats.includes("untracked"),
borderColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
compare,
"--state-unavailable-color"
),
backgroundColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
true,
compare,
"--state-unavailable-color"
),
data: untrackedConsumption,
order: 1 + this._untrackedIndex!,
stack: "devices",
pointStyle: compare ? false : "circle",
xAxisID: compare ? "xAxisCompare" : undefined,
};
const datasetExtra = {
show_legend: !compare,
};
return { dataset, datasetExtra };
}
private _processDataSet( private _processDataSet(
computedStyle: CSSStyleDeclaration, computedStyle: CSSStyleDeclaration,
statistics: Statistics, statistics: Statistics,

View File

@@ -31,7 +31,6 @@ import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
getStatisticLabel, getStatisticLabel,
isExternalStatistic,
} from "../../../../data/recorder"; } from "../../../../data/recorder";
import { FrontendLocaleData } from "../../../../data/translation"; import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@@ -39,7 +38,6 @@ import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergyDevicesGraphCardConfig } from "../types"; import { EnergyDevicesGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
import { clickIsTouch } from "../../../../components/chart/click_is_touch";
@customElement("hui-energy-devices-graph-card") @customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard export class HuiEnergyDevicesGraphCard
@@ -160,18 +158,15 @@ export class HuiEnergyDevicesGraphCard
// @ts-expect-error // @ts-expect-error
locale: numberFormatToLocale(this.hass.locale), locale: numberFormatToLocale(this.hass.locale),
onClick: (e: any) => { onClick: (e: any) => {
if (clickIsTouch(e)) return;
const chart = e.chart; const chart = e.chart;
const canvasPosition = getRelativePosition(e, chart); const canvasPosition = getRelativePosition(e, chart);
const index = Math.abs( const index = Math.abs(
chart.scales.y.getValueForPixel(canvasPosition.y) chart.scales.y.getValueForPixel(canvasPosition.y)
); );
// @ts-ignore
const statisticId = this._chartData?.datasets[0]?.data[index]?.y;
if (!statisticId || isExternalStatistic(statisticId)) return;
fireEvent(this, "hass-more-info", { fireEvent(this, "hass-more-info", {
entityId: statisticId, // @ts-ignore
entityId: this._chartData?.datasets[0]?.data[index]?.y,
}); });
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
}, },

View File

@@ -11,7 +11,6 @@ import {
PropertyValues, PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { formatNumber } from "../../../../common/number/format_number"; import { formatNumber } from "../../../../common/number/format_number";
import { getEnergyColor } from "./common/color"; import { getEnergyColor } from "./common/color";
@@ -26,14 +25,12 @@ import {
import { import {
calculateStatisticSumGrowth, calculateStatisticSumGrowth,
getStatisticLabel, getStatisticLabel,
isExternalStatistic,
} from "../../../../data/recorder"; } from "../../../../data/recorder";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types"; import { LovelaceCard } from "../../types";
import { EnergySourcesTableCardConfig } from "../types"; import { EnergySourcesTableCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
import { fireEvent } from "../../../../common/dom/fire_event";
const colorPropertyMap = { const colorPropertyMap = {
grid_return: "--energy-grid-return-color", grid_return: "--energy-grid-return-color",
@@ -228,13 +225,7 @@ export class HuiEnergySourcesTableCard
0; 0;
totalSolarCompare += compareEnergy; totalSolarCompare += compareEnergy;
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(source.stat_energy_from),
})}"
@click=${this._handleMoreInfo}
.entity=${source.stat_energy_from}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -339,13 +330,7 @@ export class HuiEnergySourcesTableCard
0; 0;
totalBatteryCompare += energyFromCompare - energyToCompare; totalBatteryCompare += energyFromCompare - energyToCompare;
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(source.stat_energy_from),
})}"
@click=${this._handleMoreInfo}
.entity=${source.stat_energy_from}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -396,13 +381,7 @@ export class HuiEnergySourcesTableCard
? html`<td class="mdc-data-table__cell"></td>` ? html`<td class="mdc-data-table__cell"></td>`
: ""} : ""}
</tr> </tr>
<tr <tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(source.stat_energy_to),
})}"
@click=${this._handleMoreInfo}
.entity=${source.stat_energy_to}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -529,13 +508,7 @@ export class HuiEnergySourcesTableCard
totalGridCostCompare += costCompare; totalGridCostCompare += costCompare;
} }
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(flow.stat_energy_from),
})}"
@click=${this._handleMoreInfo}
.entity=${flow.stat_energy_from}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -646,13 +619,7 @@ export class HuiEnergySourcesTableCard
totalGridCostCompare += costCompare; totalGridCostCompare += costCompare;
} }
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(flow.stat_energy_to),
})}"
@click=${this._handleMoreInfo}
.entity=${flow.stat_energy_to}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -817,13 +784,7 @@ export class HuiEnergySourcesTableCard
totalGasCostCompare += costCompare; totalGasCostCompare += costCompare;
} }
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(source.stat_energy_from),
})}"
@click=${this._handleMoreInfo}
.entity=${source.stat_energy_from}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -981,13 +942,7 @@ export class HuiEnergySourcesTableCard
totalWaterCostCompare += costCompare; totalWaterCostCompare += costCompare;
} }
return html`<tr return html`<tr class="mdc-data-table__row">
class="mdc-data-table__row ${classMap({
clickable: !isExternalStatistic(source.stat_energy_from),
})}"
@click=${this._handleMoreInfo}
.entity=${source.stat_energy_from}
>
<td class="mdc-data-table__cell cell-bullet"> <td class="mdc-data-table__cell cell-bullet">
<div <div
class="bullet" class="bullet"
@@ -1156,13 +1111,6 @@ export class HuiEnergySourcesTableCard
</ha-card>`; </ha-card>`;
} }
private _handleMoreInfo(ev): void {
const entityId = ev.currentTarget?.entity;
if (entityId && !isExternalStatistic(entityId)) {
fireEvent(this, "hass-more-info", { entityId });
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
${unsafeCSS(dataTableStyles)} ${unsafeCSS(dataTableStyles)}
@@ -1179,9 +1127,6 @@ export class HuiEnergySourcesTableCard
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover { .mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
background-color: rgba(var(--rgb-primary-text-color), 0.04); background-color: rgba(var(--rgb-primary-text-color), 0.04);
} }
.clickable {
cursor: pointer;
}
.total { .total {
--mdc-typography-body2-font-weight: 500; --mdc-typography-body2-font-weight: 500;
} }

View File

@@ -114,7 +114,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
return { return {
grid_columns: "full", grid_columns: 4,
grid_rows: 4, grid_rows: 4,
grid_min_rows: 2, grid_min_rows: 2,
}; };

View File

@@ -426,7 +426,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
return { return {
grid_columns: "full", grid_columns: 4,
grid_rows: 4, grid_rows: 4,
grid_min_columns: 2, grid_min_columns: 2,
grid_min_rows: 2, grid_min_rows: 2,

View File

@@ -1,36 +0,0 @@
import { conditionalClamp } from "../../../common/number/clamp";
import { LovelaceLayoutOptions } from "../types";
export const DEFAULT_GRID_SIZE = {
columns: 4,
rows: "auto",
} as CardGridSize;
export type CardGridSize = {
rows: number | "auto";
columns: number | "full";
};
export const computeCardGridSize = (
options: LovelaceLayoutOptions
): CardGridSize => {
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.grid_min_rows;
const maxRows = options.grid_max_rows;
const minColumns = options.grid_min_columns;
const maxColumns = options.grid_max_columns;
const clampedRows =
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
const clampedColumns =
typeof columns === "string"
? columns
: conditionalClamp(columns, minColumns, maxColumns);
return {
rows: clampedRows,
columns: clampedColumns,
};
};

View File

@@ -8,7 +8,6 @@ import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
import { HuiElementEditor } from "../hui-element-editor"; import { HuiElementEditor } from "../hui-element-editor";
import "./hui-card-layout-editor"; import "./hui-card-layout-editor";
import "./hui-card-visibility-editor"; import "./hui-card-visibility-editor";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
const tabs = ["config", "visibility", "layout"] as const; const tabs = ["config", "visibility", "layout"] as const;
@@ -17,7 +16,8 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
@property({ type: Boolean, attribute: "show-visibility-tab" }) @property({ type: Boolean, attribute: "show-visibility-tab" })
public showVisibilityTab = false; public showVisibilityTab = false;
@property({ attribute: false }) public sectionConfig?: LovelaceSectionConfig; @property({ type: Boolean, attribute: "show-layout-tab" })
public showLayoutTab = false;
@state() private _currTab: (typeof tabs)[number] = tabs[0]; @state() private _currTab: (typeof tabs)[number] = tabs[0];
@@ -48,18 +48,10 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
this.value = ev.detail.value; this.value = ev.detail.value;
} }
get _showLayoutTab(): boolean {
return (
!!this.sectionConfig &&
(this.sectionConfig.type === undefined ||
this.sectionConfig.type === "grid")
);
}
protected renderConfigElement(): TemplateResult { protected renderConfigElement(): TemplateResult {
const displayedTabs: string[] = ["config"]; const displayedTabs: string[] = ["config"];
if (this.showVisibilityTab) displayedTabs.push("visibility"); if (this.showVisibilityTab) displayedTabs.push("visibility");
if (this._showLayoutTab) displayedTabs.push("layout"); if (this.showLayoutTab) displayedTabs.push("layout");
if (displayedTabs.length === 1) return super.renderConfigElement(); if (displayedTabs.length === 1) return super.renderConfigElement();
@@ -83,7 +75,6 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
<hui-card-layout-editor <hui-card-layout-editor
.hass=${this.hass} .hass=${this.hass}
.config=${this.value} .config=${this.value}
.sectionConfig=${this.sectionConfig!}
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
> >
</hui-card-layout-editor> </hui-card-layout-editor>

View File

@@ -1,8 +1,7 @@
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js"; import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, nothing, PropertyValues } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default"; import { preventDefault } from "../../../../common/dom/prevent_default";
@@ -19,14 +18,10 @@ import "../../../../components/ha-switch";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { HuiCard } from "../../cards/hui-card"; import { HuiCard } from "../../cards/hui-card";
import { import { computeSizeOnGrid } from "../../sections/hui-grid-section";
CardGridSize,
computeCardGridSize,
} from "../../common/compute-card-grid-size";
import { LovelaceLayoutOptions } from "../../types"; import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor") @customElement("hui-card-layout-editor")
@@ -35,8 +30,6 @@ export class HuiCardLayoutEditor extends LitElement {
@property({ attribute: false }) public config!: LovelaceCardConfig; @property({ attribute: false }) public config!: LovelaceCardConfig;
@property({ attribute: false }) public sectionConfig!: LovelaceSectionConfig;
@state() _defaultLayoutOptions?: LovelaceLayoutOptions; @state() _defaultLayoutOptions?: LovelaceLayoutOptions;
@state() public _yamlMode = false; @state() public _yamlMode = false;
@@ -57,7 +50,7 @@ export class HuiCardLayoutEditor extends LitElement {
}) })
); );
private _computeCardGridSize = memoizeOne(computeCardGridSize); private _gridSizeValue = memoizeOne(computeSizeOnGrid);
private _isDefault = memoizeOne( private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) => (options?: LovelaceLayoutOptions) =>
@@ -70,9 +63,7 @@ export class HuiCardLayoutEditor extends LitElement {
this._defaultLayoutOptions this._defaultLayoutOptions
); );
const value = this._computeCardGridSize(options); const sizeValue = this._gridSizeValue(options);
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
return html` return html`
<div class="header"> <div class="header">
@@ -136,12 +127,8 @@ export class HuiCardLayoutEditor extends LitElement {
` `
: html` : html`
<ha-grid-size-picker <ha-grid-size-picker
style=${styleMap({
"max-width": `${totalColumns * 45 + 50}px`,
})}
.columns=${totalColumns}
.hass=${this.hass} .hass=${this.hass}
.value=${value} .value=${sizeValue}
.isDefault=${this._isDefault(this.config.layout_options)} .isDefault=${this._isDefault(this.config.layout_options)}
@value-changed=${this._gridSizeChanged} @value-changed=${this._gridSizeChanged}
.rowMin=${options.grid_min_rows} .rowMin=${options.grid_min_rows}
@@ -149,24 +136,6 @@ export class HuiCardLayoutEditor extends LitElement {
.columnMin=${options.grid_min_columns} .columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns} .columnMax=${options.grid_max_columns}
></ha-grid-size-picker> ></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width"
)}
</span>
<span slot="description" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width_helper"
)}
</span>
<ha-switch
@change=${this._fullWidthChanged}
.checked=${value.columns === "full"}
name="full-width"
>
</ha-switch>
</ha-settings-row>
`} `}
`; `;
} }
@@ -226,7 +195,7 @@ export class HuiCardLayoutEditor extends LitElement {
private _gridSizeChanged(ev: CustomEvent): void { private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value as CardGridSize; const value = ev.detail.value;
const newConfig: LovelaceCardConfig = { const newConfig: LovelaceCardConfig = {
...this.config, ...this.config,
@@ -260,21 +229,6 @@ export class HuiCardLayoutEditor extends LitElement {
fireEvent(this, "value-changed", { value: newConfig }); fireEvent(this, "value-changed", { value: newConfig });
} }
private _fullWidthChanged(ev): void {
ev.stopPropagation();
const value = ev.target.checked;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value
? "full"
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
},
};
fireEvent(this, "value-changed", { value: newConfig });
}
static styles = [ static styles = [
haStyle, haStyle,
css` css`
@@ -301,6 +255,7 @@ export class HuiCardLayoutEditor extends LitElement {
} }
ha-grid-size-picker { ha-grid-size-picker {
display: block; display: block;
max-width: 250px;
margin: 16px auto; margin: 16px auto;
} }
ha-yaml-editor { ha-yaml-editor {

View File

@@ -236,10 +236,8 @@ export class HuiDialogEditCard
<div class="content"> <div class="content">
<div class="element-editor"> <div class="element-editor">
<hui-card-element-editor <hui-card-element-editor
.showLayoutTab=${this._shouldShowLayoutTab()}
.showVisibilityTab=${this._cardConfig?.type !== "conditional"} .showVisibilityTab=${this._cardConfig?.type !== "conditional"}
.sectionConfig=${this._isInSection
? this._containerConfig
: undefined}
.hass=${this.hass} .hass=${this.hass}
.lovelace=${this._params.lovelaceConfig} .lovelace=${this._params.lovelaceConfig}
.value=${this._cardConfig} .value=${this._cardConfig}
@@ -355,6 +353,18 @@ export class HuiDialogEditCard
return this._params!.path.length === 2; return this._params!.path.length === 2;
} }
private _shouldShowLayoutTab(): boolean {
/**
* Only show layout tab for cards in a grid section
* In the future, every section and view should be able to bring their own editor for layout.
* For now, we limit it to grid sections as it's the only section type
* */
return (
this._isInSection &&
(!this._containerConfig.type || this._containerConfig.type === "grid")
);
}
private _cardConfigInSection = memoizeOne( private _cardConfigInSection = memoizeOne(
(cardConfig?: LovelaceCardConfig) => { (cardConfig?: LovelaceCardConfig) => {
const { cards, title, ...containerConfig } = this const { cards, title, ...containerConfig } = this

View File

@@ -22,9 +22,8 @@ import type {
} from "../../../../components/ha-form/types"; } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
DEFAULT_CONFIG, DEFAULT_DISPLAY_TYPE,
DISPLAY_TYPES, DISPLAY_TYPES,
migrateLegacyEntityBadgeConfig,
} from "../../badges/hui-entity-badge"; } from "../../badges/hui-entity-badge";
import { EntityBadgeConfig } from "../../badges/types"; import { EntityBadgeConfig } from "../../badges/types";
import type { LovelaceBadgeEditor } from "../../types"; import type { LovelaceBadgeEditor } from "../../types";
@@ -43,12 +42,10 @@ const badgeConfigStruct = assign(
icon: optional(string()), icon: optional(string()),
state_content: optional(union([string(), array(string())])), state_content: optional(union([string(), array(string())])),
color: optional(string()), color: optional(string()),
show_name: optional(boolean()),
show_state: optional(boolean()),
show_icon: optional(boolean()),
show_entity_picture: optional(boolean()), show_entity_picture: optional(boolean()),
tap_action: optional(actionConfigStruct), tap_action: optional(actionConfigStruct),
image: optional(string()), // For old badge config support show_name: optional(boolean()),
image: optional(string()),
}) })
); );
@@ -63,10 +60,7 @@ export class HuiEntityBadgeEditor
public setConfig(config: EntityBadgeConfig): void { public setConfig(config: EntityBadgeConfig): void {
assert(config, badgeConfigStruct); assert(config, badgeConfigStruct);
this._config = { this._config = config;
...DEFAULT_CONFIG,
...migrateLegacyEntityBadgeConfig(config),
};
} }
private _schema = memoizeOne( private _schema = memoizeOne(
@@ -74,11 +68,25 @@ export class HuiEntityBadgeEditor
[ [
{ name: "entity", selector: { entity: {} } }, { name: "entity", selector: { entity: {} } },
{ {
name: "appearance", name: "",
type: "expandable", type: "expandable",
flatten: true,
iconPath: mdiPalette, iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.badge.entity.appearance`),
schema: [ schema: [
{
name: "display_type",
selector: {
select: {
mode: "dropdown",
options: DISPLAY_TYPES.map((type) => ({
value: type,
label: localize(
`ui.panel.lovelace.editor.badge.entity.display_type_options.${type}`
),
})),
},
},
},
{ {
name: "", name: "",
type: "grid", type: "grid",
@@ -89,12 +97,6 @@ export class HuiEntityBadgeEditor
text: {}, text: {},
}, },
}, },
{
name: "color",
selector: {
ui_color: { default_color: true },
},
},
{ {
name: "icon", name: "icon",
selector: { selector: {
@@ -102,6 +104,12 @@ export class HuiEntityBadgeEditor
}, },
context: { icon_entity: "entity" }, context: { icon_entity: "entity" },
}, },
{
name: "color",
selector: {
ui_color: { default_color: true },
},
},
{ {
name: "show_entity_picture", name: "show_entity_picture",
selector: { selector: {
@@ -110,35 +118,7 @@ export class HuiEntityBadgeEditor
}, },
], ],
}, },
{
name: "displayed_elements",
selector: {
select: {
mode: "list",
multiple: true,
options: [
{
value: "name",
label: localize(
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.name`
),
},
{
value: "state",
label: localize(
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.state`
),
},
{
value: "icon",
label: localize(
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.icon`
),
},
],
},
},
},
{ {
name: "state_content", name: "state_content",
selector: { selector: {
@@ -153,9 +133,9 @@ export class HuiEntityBadgeEditor
], ],
}, },
{ {
name: "interactions", name: "",
type: "expandable", type: "expandable",
flatten: true, title: localize(`ui.panel.lovelace.editor.badge.entity.interactions`),
iconPath: mdiGestureTap, iconPath: mdiGestureTap,
schema: [ schema: [
{ {
@@ -171,20 +151,6 @@ export class HuiEntityBadgeEditor
] as const satisfies readonly HaFormSchema[] ] as const satisfies readonly HaFormSchema[]
); );
_displayedElements = memoizeOne((config: EntityBadgeConfig) => {
const elements: string[] = [];
if (config.show_name) {
elements.push("name");
}
if (config.show_state) {
elements.push("state");
}
if (config.show_icon) {
elements.push("icon");
}
return elements;
});
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return nothing; return nothing;
@@ -192,10 +158,11 @@ export class HuiEntityBadgeEditor
const schema = this._schema(this.hass!.localize); const schema = this._schema(this.hass!.localize);
const data = { const data = { ...this._config };
...this._config,
displayed_elements: this._displayedElements(this._config), if (!data.display_type) {
}; data.display_type = DEFAULT_DISPLAY_TYPE;
}
return html` return html`
<ha-form <ha-form
@@ -214,17 +181,18 @@ export class HuiEntityBadgeEditor
return; return;
} }
const config = { ...ev.detail.value } as EntityBadgeConfig; const newConfig = ev.detail.value as EntityBadgeConfig;
const config: EntityBadgeConfig = {
...newConfig,
};
if (!config.state_content) { if (!config.state_content) {
delete config.state_content; delete config.state_content;
} }
if (config.displayed_elements) { if (config.display_type === "standard") {
config.show_name = config.displayed_elements.includes("name"); delete config.display_type;
config.show_state = config.displayed_elements.includes("state");
config.show_icon = config.displayed_elements.includes("icon");
delete config.displayed_elements;
} }
fireEvent(this, "config-changed", { config }); fireEvent(this, "config-changed", { config });
@@ -236,10 +204,8 @@ export class HuiEntityBadgeEditor
switch (schema.name) { switch (schema.name) {
case "color": case "color":
case "state_content": case "state_content":
case "display_type":
case "show_entity_picture": case "show_entity_picture":
case "displayed_elements":
case "appearance":
case "interactions":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.badge.entity.${schema.name}` `ui.panel.lovelace.editor.badge.entity.${schema.name}`
); );

View File

@@ -30,11 +30,11 @@ export class HuiStateLabelBadgeEditor extends HuiEntityBadgeEditor {
const entityBadgeConfig: EntityBadgeConfig = { const entityBadgeConfig: EntityBadgeConfig = {
type: "entity", type: "entity",
entity: config.entity, entity: config.entity,
show_name: config.show_name ?? true, display_type: config.show_name === false ? "standard" : "complete",
}; };
// @ts-ignore // @ts-ignore
super.setConfig(entityBadgeConfig); this._config = entityBadgeConfig;
} }
} }

View File

@@ -14,6 +14,7 @@ import {
union, union,
} from "superstruct"; } from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
HaFormSchema, HaFormSchema,
@@ -68,14 +69,18 @@ export class HuiTileCardEditor
} }
private _schema = memoizeOne( private _schema = memoizeOne(
(entityId: string | undefined, hideState: boolean) => (
localize: LocalizeFunc,
entityId: string | undefined,
hideState: boolean
) =>
[ [
{ name: "entity", selector: { entity: {} } }, { name: "entity", selector: { entity: {} } },
{ {
name: "appearance", name: "",
flatten: true,
type: "expandable", type: "expandable",
iconPath: mdiPalette, iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.card.tile.appearance`),
schema: [ schema: [
{ {
name: "", name: "",
@@ -131,9 +136,9 @@ export class HuiTileCardEditor
], ],
}, },
{ {
name: "interactions", name: "",
type: "expandable", type: "expandable",
flatten: true, title: localize(`ui.panel.lovelace.editor.card.tile.interactions`),
iconPath: mdiGestureTap, iconPath: mdiGestureTap,
schema: [ schema: [
{ {
@@ -173,6 +178,7 @@ export class HuiTileCardEditor
: undefined; : undefined;
const schema = this._schema( const schema = this._schema(
this.hass!.localize,
this._config.entity, this._config.entity,
this._config.hide_state ?? false this._config.hide_state ?? false
); );
@@ -300,8 +306,6 @@ export class HuiTileCardEditor
case "vertical": case "vertical":
case "hide_state": case "hide_state":
case "state_content": case "state_content":
case "appearance":
case "interactions":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.${schema.name}` `ui.panel.lovelace.editor.card.tile.${schema.name}`
); );

View File

@@ -35,7 +35,6 @@ import "./hui-section-visibility-editor";
import type { EditSectionDialogParams } from "./show-edit-section-dialog"; import type { EditSectionDialogParams } from "./show-edit-section-dialog";
import "@material/mwc-tab-bar/mwc-tab-bar"; import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab"; import "@material/mwc-tab/mwc-tab";
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
const TABS = ["tab-settings", "tab-visibility"] as const; const TABS = ["tab-settings", "tab-visibility"] as const;
@@ -50,8 +49,6 @@ export class HuiDialogEditSection
@state() private _config?: LovelaceSectionRawConfig; @state() private _config?: LovelaceSectionRawConfig;
@state() private _viewConfig?: LovelaceViewConfig;
@state() private _yamlMode = false; @state() private _yamlMode = false;
@state() private _currTab: (typeof TABS)[number] = TABS[0]; @state() private _currTab: (typeof TABS)[number] = TABS[0];
@@ -60,10 +57,10 @@ export class HuiDialogEditSection
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (this._yamlMode && changedProperties.has("_yamlMode")) { if (this._yamlMode && changedProperties.has("_yamlMode")) {
const sectionConfig = { const viewConfig = {
...this._config, ...this._config,
}; };
this._editor?.setValue(sectionConfig); this._editor?.setValue(viewConfig);
} }
} }
@@ -74,9 +71,6 @@ export class HuiDialogEditSection
this._params.viewIndex, this._params.viewIndex,
this._params.sectionIndex, this._params.sectionIndex,
]); ]);
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
this._params.viewIndex,
]);
} }
public closeDialog() { public closeDialog() {
@@ -113,7 +107,6 @@ export class HuiDialogEditSection
<hui-section-settings-editor <hui-section-settings-editor
.hass=${this.hass} .hass=${this.hass}
.config=${this._config} .config=${this._config}
.viewConfig=${this._viewConfig}
@value-changed=${this._configChanged} @value-changed=${this._configChanged}
> >
</hui-section-settings-editor> </hui-section-settings-editor>

View File

@@ -1,19 +1,22 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { import {
HaFormSchema, HaFormSchema,
SchemaUnion, SchemaUnion,
} from "../../../../components/ha-form/types"; } from "../../../../components/ha-form/types";
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section"; import { fireEvent } from "../../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import { HomeAssistant } from "../../../../types"; const SCHEMA = [
{
name: "title",
selector: { text: {} },
},
] as const satisfies HaFormSchema[];
type SettingsData = { type SettingsData = {
title: string; title: string;
column_span?: number;
}; };
@customElement("hui-section-settings-editor") @customElement("hui-section-settings-editor")
@@ -22,46 +25,16 @@ export class HuiDialogEditSection extends LitElement {
@property({ attribute: false }) public config!: LovelaceSectionRawConfig; @property({ attribute: false }) public config!: LovelaceSectionRawConfig;
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
private _schema = memoizeOne(
(localize: LocalizeFunc, maxColumns: number) =>
[
{
name: "title",
selector: { text: {} },
},
{
name: "column_span",
selector: {
number: {
min: 1,
max: maxColumns,
unit_of_measurement: localize(
`ui.panel.lovelace.editor.edit_section.settings.column_span_unit`
),
},
},
},
] as const satisfies HaFormSchema[]
);
render() { render() {
const data: SettingsData = { const data: SettingsData = {
title: this.config.title || "", title: this.config.title || "",
column_span: this.config.column_span || 1,
}; };
const schema = this._schema(
this.hass.localize,
this.viewConfig.max_columns || 4
);
return html` return html`
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${data} .data=${data}
.schema=${schema} .schema=${SCHEMA}
.computeLabel=${this._computeLabel} .computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper} .computeHelper=${this._computeHelper}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@@ -69,16 +42,12 @@ export class HuiDialogEditSection extends LitElement {
`; `;
} }
private _computeLabel = ( private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass.localize( this.hass.localize(
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}` `ui.panel.lovelace.editor.edit_section.settings.${schema.name}`
); );
private _computeHelper = ( private _computeHelper = (schema: SchemaUnion<typeof SCHEMA>) =>
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass.localize( this.hass.localize(
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper` `ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper`
) || ""; ) || "";
@@ -90,7 +59,6 @@ export class HuiDialogEditSection extends LitElement {
const newConfig: LovelaceSectionRawConfig = { const newConfig: LovelaceSectionRawConfig = {
...this.config, ...this.config,
title: newData.title, title: newData.title,
column_span: newData.column_span,
}; };
if (!newConfig.title) { if (!newConfig.title) {

View File

@@ -14,8 +14,8 @@ import type { HomeAssistant } from "../../../types";
import { HuiCard } from "../cards/hui-card"; import { HuiCard } from "../cards/hui-card";
import "../components/hui-card-edit-mode"; import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util"; import { moveCard } from "../editor/config-util";
import type { Lovelace } from "../types"; import type { Lovelace, LovelaceLayoutOptions } from "../types";
import { computeCardGridSize } from "../common/compute-card-grid-size"; import { conditionalClamp } from "../../../common/number/clamp";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = { const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100, delay: 100,
@@ -24,6 +24,43 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7, invertedSwapThreshold: 0.7,
} as HaSortableOptions; } as HaSortableOptions;
export const DEFAULT_GRID_OPTIONS = {
grid_columns: 4,
grid_rows: "auto",
} as const satisfies LovelaceLayoutOptions;
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
export const computeSizeOnGrid = (
options: LovelaceLayoutOptions
): GridSizeValue => {
const rows =
typeof options.grid_rows === "number"
? conditionalClamp(
options.grid_rows,
options.grid_min_rows,
options.grid_max_rows
)
: DEFAULT_GRID_OPTIONS.grid_rows;
const columns =
typeof options.grid_columns === "number"
? conditionalClamp(
options.grid_columns,
options.grid_min_columns,
options.grid_max_columns
)
: DEFAULT_GRID_OPTIONS.grid_columns;
return {
rows,
columns,
};
};
export class GridSection extends LitElement implements LovelaceSectionElement { export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -97,18 +134,16 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
card.layout = "grid"; card.layout = "grid";
const layoutOptions = card.getLayoutOptions(); const layoutOptions = card.getLayoutOptions();
const { rows, columns } = computeCardGridSize(layoutOptions); const { rows, columns } = computeSizeOnGrid(layoutOptions);
return html` return html`
<div <div
style=${styleMap({ style=${styleMap({
"--column-size": "--column-size": columns,
typeof columns === "number" ? columns : undefined, "--row-size": rows,
"--row-size": typeof rows === "number" ? rows : undefined,
})} })}
class="card ${classMap({ class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number", "fit-rows": typeof layoutOptions?.grid_rows === "number",
"full-width": columns === "full",
})}" })}"
> >
${editMode ${editMode
@@ -176,7 +211,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
haStyle, haStyle,
css` css`
:host { :host {
--base-column-count: 4; --column-count: 4;
--row-gap: var(--ha-section-grid-row-gap, 8px); --row-gap: var(--ha-section-grid-row-gap, 8px);
--column-gap: var(--ha-section-grid-column-gap, 8px); --column-gap: var(--ha-section-grid-column-gap, 8px);
--row-height: var(--ha-section-grid-row-height, 56px); --row-height: var(--ha-section-grid-row-height, 56px);
@@ -185,14 +220,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
gap: var(--row-gap); gap: var(--row-gap);
} }
.container { .container {
--grid-column-count: calc(
var(--base-column-count) * var(--column-span, 1)
);
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(var(--column-count), minmax(0, 1fr));
var(--grid-column-count),
minmax(0, 1fr)
);
grid-auto-rows: minmax(var(--row-height), auto); grid-auto-rows: minmax(var(--row-height), auto);
row-gap: var(--row-gap); row-gap: var(--row-gap);
column-gap: var(--column-gap); column-gap: var(--column-gap);
@@ -233,8 +262,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.card { .card {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
position: relative; position: relative;
grid-row: span var(--row-size, 1); grid-row: span var(--row-size);
grid-column: span min(var(--column-size, 1), var(--grid-column-count)); grid-column: span var(--column-size);
} }
.card.fit-rows { .card.fit-rows {
@@ -245,10 +274,6 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
); );
} }
.card.full-width {
grid-column: 1 / -1;
}
.card:has(> *) { .card:has(> *) {
display: block; display: block;
} }

View File

@@ -42,7 +42,7 @@ export interface LovelaceBadge extends HTMLElement {
} }
export type LovelaceLayoutOptions = { export type LovelaceLayoutOptions = {
grid_columns?: number | "full"; grid_columns?: number;
grid_rows?: number | "auto"; grid_rows?: number | "auto";
grid_max_columns?: number; grid_max_columns?: number;
grid_min_columns?: number; grid_min_columns?: number;

View File

@@ -1,4 +1,3 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js"; import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
import { import {
CSSResultGroup, CSSResultGroup,
@@ -27,10 +26,6 @@ import { showEditSectionDialog } from "../editor/section-editor/show-edit-sectio
import { HuiSection } from "../sections/hui-section"; import { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types"; import type { Lovelace } from "../types";
export const DEFAULT_MAX_COLUMNS = 4;
const parsePx = (value: string) => parseInt(value.replace("px", ""));
@customElement("hui-sections-view") @customElement("hui-sections-view")
export class SectionsView extends LitElement implements LovelaceViewElement { export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -51,30 +46,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@state() _dragging = false; @state() _dragging = false;
private _columnsController = new ResizeController(this, {
callback: (entries) => {
const totalWidth = entries[0]?.contentRect.width;
const style = getComputedStyle(this);
const container = this.shadowRoot!.querySelector(".container")!;
const containerStyle = getComputedStyle(container);
const paddingLeft = parsePx(containerStyle.paddingLeft);
const paddingRight = parsePx(containerStyle.paddingRight);
const padding = paddingLeft + paddingRight;
const minColumnWidth = parsePx(
style.getPropertyValue("--column-min-width")
);
const columnGap = parsePx(containerStyle.columnGap);
const columns = Math.floor(
(totalWidth - padding + columnGap) / (minColumnWidth + columnGap)
);
const maxColumns = this._config?.max_columns ?? DEFAULT_MAX_COLUMNS;
return Math.max(Math.min(maxColumns, columns), 1);
},
});
public setConfig(config: LovelaceViewConfig): void { public setConfig(config: LovelaceViewConfig): void {
this._config = config; this._config = config;
} }
@@ -124,11 +95,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
if (!this.lovelace) return nothing; if (!this.lovelace) return nothing;
const sections = this.sections; const sections = this.sections;
const totalSectionCount = const totalCount = this._sectionCount + (this.lovelace?.editMode ? 1 : 0);
this._sectionCount + (this.lovelace?.editMode ? 1 : 0);
const editMode = this.lovelace.editMode; const editMode = this.lovelace.editMode;
const maxColumnCount = this._columnsController.value ?? 1; const maxColumnsCount = this._config?.max_columns;
return html` return html`
<hui-view-badges <hui-view-badges
@@ -148,29 +118,17 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
<div <div
class="container" class="container"
style=${styleMap({ style=${styleMap({
"--total-section-count": totalSectionCount, "--max-columns-count": maxColumnsCount,
"--max-column-count": maxColumnCount, "--total-count": totalCount,
})} })}
> >
${repeat( ${repeat(
sections, sections,
(section) => this._getSectionKey(section), (section) => this._getSectionKey(section),
(section, idx) => { (section, idx) => {
const sectionConfig = this._config?.sections?.[idx];
const columnSpan = Math.min(
sectionConfig?.column_span || 1,
maxColumnCount
);
(section as any).itemPath = [idx]; (section as any).itemPath = [idx];
return html` return html`
<div <div class="section">
class="section"
style=${styleMap({
"--column-span": columnSpan,
})}
>
${editMode ${editMode
? html` ? html`
<div class="section-overlay"> <div class="section-overlay">
@@ -294,19 +252,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
--row-height: var(--ha-view-sections-row-height, 56px); --row-height: var(--ha-view-sections-row-height, 56px);
--row-gap: var(--ha-view-sections-row-gap, 8px); --row-gap: var(--ha-view-sections-row-gap, 8px);
--column-gap: var(--ha-view-sections-column-gap, 32px); --column-gap: var(--ha-view-sections-column-gap, 32px);
--column-max-width: var(--ha-view-sections-column-max-width, 500px);
--column-min-width: var(--ha-view-sections-column-min-width, 320px); --column-min-width: var(--ha-view-sections-column-min-width, 320px);
--column-max-width: var(--ha-view-sections-column-max-width, 500px);
display: block; display: block;
} }
.container > * { .container > * {
position: relative; position: relative;
max-width: var(--column-max-width);
width: 100%; width: 100%;
} }
.section { .section {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
grid-column: span var(--column-span);
} }
.section:not(:has(> *:not([hidden]))) { .section:not(:has(> *:not([hidden]))) {
@@ -314,22 +272,29 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
} }
.container { .container {
--column-count: min( --max-count: min(var(--total-count), var(--max-columns-count, 4));
var(--max-column-count), --max-width: min(
var(--total-section-count) calc(
(var(--max-count) + 1) * var(--column-min-width) +
(var(--max-count) + 2) * var(--column-gap) - 1px
),
calc(
var(--max-count) * var(--column-max-width) + (var(--max-count) + 1) *
var(--column-gap)
)
); );
display: grid; display: grid;
align-items: start; align-items: start;
justify-content: center; justify-items: center;
grid-template-columns: repeat(var(--column-count), 1fr); grid-template-columns: repeat(
auto-fit,
minmax(min(var(--column-min-width), 100%), 1fr)
);
gap: var(--row-gap) var(--column-gap); gap: var(--row-gap) var(--column-gap);
padding: var(--row-gap) var(--column-gap); padding: var(--row-gap) var(--column-gap);
box-sizing: content-box; box-sizing: border-box;
max-width: var(--max-width);
margin: 0 auto; margin: 0 auto;
max-width: calc(
var(--column-count) * var(--column-max-width) +
(var(--column-count) - 1) * var(--column-gap)
);
} }
@media (max-width: 600px) { @media (max-width: 600px) {

View File

@@ -1538,10 +1538,7 @@
}, },
"schedule": { "schedule": {
"delete": "Delete item?", "delete": "Delete item?",
"confirm_delete": "Do you want to delete this item?", "confirm_delete": "Do you want to delete this item?"
"edit_schedule_block": "Edit schedule block",
"start": "Start",
"end": "End"
}, },
"template": { "template": {
"time": "[%key:ui::panel::developer-tools::tabs::templates::time%]", "time": "[%key:ui::panel::developer-tools::tabs::templates::time%]",
@@ -4701,12 +4698,9 @@
"title": "Change network channel", "title": "Change network channel",
"new_channel": "New channel", "new_channel": "New channel",
"change_channel": "Change channel", "change_channel": "Change channel",
"migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating! It may take up to an hour for changes to propagate to all devices.", "migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating!",
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz WiFi networks on the same channel, motherboards, and so on.",
"smart_explanation": "It is recommended to use the \"Smart\" option once your environment is optimized as opposed to manually choosing a channel, as it picks the best channel for you after scanning all Zigbee channels. This does not configure ZHA to automatically change channels in the future, it only changes the channel a single time.",
"channel_has_been_changed": "Network channel has been changed", "channel_has_been_changed": "Network channel has been changed",
"devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes.", "devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes."
"channel_auto": "Smart"
} }
}, },
"zwave_js": { "zwave_js": {
@@ -5588,10 +5582,6 @@
"tab_layout": "Layout", "tab_layout": "Layout",
"visibility": { "visibility": {
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown." "explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
},
"layout": {
"full_width": "Full width card",
"full_width_helper": "Take up the full width of the section whatever its size"
} }
}, },
"edit_badge": { "edit_badge": {
@@ -5661,10 +5651,7 @@
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]", "edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
"settings": { "settings": {
"title": "Title", "title": "Title",
"title_helper": "The title will appear at the top of section. Leave empty to hide the title.", "title_helper": "The title will appear at the top of section. Leave empty to hide the title."
"column_span": "Size",
"column_span_unit": "columns",
"column_span_helper": "The size may be smaller if less columns are displayed (e.g. on mobile devices)."
}, },
"visibility": { "visibility": {
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown." "explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
@@ -5966,9 +5953,9 @@
"paste": "Paste from clipboard", "paste": "Paste from clipboard",
"paste_description": "Paste a {type} card from the clipboard", "paste_description": "Paste a {type} card from the clipboard",
"refresh_interval": "Refresh interval", "refresh_interval": "Refresh interval",
"show_icon": "Show icon", "show_icon": "Show icon?",
"show_name": "Show name", "show_name": "Show name?",
"show_state": "Show state", "show_state": "Show state?",
"tap_action": "Tap behavior", "tap_action": "Tap behavior",
"title": "Title", "title": "Title",
"theme": "Theme", "theme": "Theme",
@@ -6108,11 +6095,11 @@
"appearance": "Appearance", "appearance": "Appearance",
"show_entity_picture": "Show entity picture", "show_entity_picture": "Show entity picture",
"state_content": "State content", "state_content": "State content",
"displayed_elements": "Displayed elements", "display_type": "Display type",
"displayed_elements_options": { "display_type_options": {
"icon": "Icon", "minimal": "Minimal (icon only)",
"name": "Name", "standard": "Standard (icon and state)",
"state": "State" "complete": "Complete (icon, name and state)"
} }
}, },
"generic": { "generic": {
@@ -7158,8 +7145,7 @@
"charts": { "charts": {
"stat_house_energy_meter": "Total energy consumption", "stat_house_energy_meter": "Total energy consumption",
"solar": "Solar", "solar": "Solar",
"by_device": "Consumption by device", "by_device": "Consumption by device"
"untracked_consumption": "Untracked consumption"
}, },
"cards": { "cards": {
"energy_usage_graph_title": "Energy usage", "energy_usage_graph_title": "Energy usage",

View File

@@ -1511,14 +1511,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@codemirror/view@npm:6.33.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0": "@codemirror/view@npm:6.32.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.33.0 version: 6.32.0
resolution: "@codemirror/view@npm:6.33.0" resolution: "@codemirror/view@npm:6.32.0"
dependencies: dependencies:
"@codemirror/state": "npm:^6.4.0" "@codemirror/state": "npm:^6.4.0"
style-mod: "npm:^4.1.0" style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4" w3c-keyname: "npm:^2.2.4"
checksum: 10/240f1b5ed6ddbc928b220e241e7c67d2f8aaa04af337729cd80ea435c84fca02fe4136d2d4750a978d39c20e56f5ce332e6af2620c2e72d7bede35eebbf9e8ee checksum: 10/bd67efbf692027175bff3a90620f32776eab203edd1bb4ccd57043203b30c12de65cac0ca5a36ab718c7f8d7105475905515383862d0c1ccf80b8412d9a3f295
languageName: node languageName: node
linkType: hard linkType: hard
@@ -8927,7 +8927,7 @@ __metadata:
"@codemirror/legacy-modes": "npm:6.4.1" "@codemirror/legacy-modes": "npm:6.4.1"
"@codemirror/search": "npm:6.5.6" "@codemirror/search": "npm:6.5.6"
"@codemirror/state": "npm:6.4.1" "@codemirror/state": "npm:6.4.1"
"@codemirror/view": "npm:6.33.0" "@codemirror/view": "npm:6.32.0"
"@egjs/hammerjs": "npm:2.0.17" "@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.12.5" "@formatjs/intl-datetimeformat": "npm:6.12.5"
"@formatjs/intl-displaynames": "npm:6.6.8" "@formatjs/intl-displaynames": "npm:6.6.8"