Compare commits

..

12 Commits

Author SHA1 Message Date
Jan-Philipp Benecke
3a0642a610 Improve ZHA config dashboard styling 2025-10-13 23:01:24 +02:00
Copilot
9eb5360a68 Update add-on auto-update strings to use full phrase (#27484)
Co-authored-by: balloob <1444314+balloob@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-10-13 16:38:38 +02:00
Aidan Timson
e9e32c7d91 Migrate restart wait to ha-wa-dialog (#27476)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-13 13:52:44 +00:00
Wendelin
c83d760e82 Enable keyboard resizing of automation sidebar (#27473) 2025-10-13 15:50:27 +02:00
Paul Bottein
489b7f9227 Revert "Add plus and minus button for media player more info" (#27409) 2025-10-13 15:43:44 +02:00
Paul Bottein
ad2ba63155 Use entity naming in cards and badges (#27428) 2025-10-13 15:43:17 +02:00
Petar Petrov
29bc894dbd Improve sampling in trend feature and sensor card (#27190) 2025-10-13 15:36:33 +02:00
karwosts
faf6cb6333 Disconnect streaming <img> when closing media dialog (#27479) 2025-10-13 15:57:02 +03:00
Aidan Timson
a2e1e6362b Migrate new backup dialog to ha-wa-dialog (#27430) 2025-10-13 13:13:26 +02:00
Paul Bottein
3212ab6f3b Align more info breadcrumb style with entity picker style for context (#27447) 2025-10-13 10:59:04 +03:00
Paul Bottein
3d27daad80 Merge favorite and common controls in home dashboard (#27438) 2025-10-13 10:55:23 +03:00
Dave T
b679f1ce60 Remove trailing whitespace from ZHA pairing doc link (#27468)
* Remove trailing whitespace from doc link

* format

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-13 06:47:03 +00:00
54 changed files with 983 additions and 926 deletions

View File

@@ -9,6 +9,11 @@ import { getEntityContext } from "./context/get_entity_context";
const DEFAULT_SEPARATOR = " ";
export const DEFAULT_ENTITY_NAME = [
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
export type EntityNameItem =
| {
type: "entity" | "device" | "area" | "floor";
@@ -24,14 +29,14 @@ export interface EntityNameOptions {
export const computeEntityNameDisplay = (
stateObj: HassEntity,
name: EntityNameItem | EntityNameItem[],
name: EntityNameItem | EntityNameItem[] | undefined,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
options?: EntityNameOptions
) => {
let items = ensureArray(name);
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
const separator = options?.separator ?? DEFAULT_SEPARATOR;

View File

@@ -1,21 +1,22 @@
import type { LineSeriesOption } from "echarts";
export function downSampleLineData(
data: LineSeriesOption["data"],
chartWidth: number,
export function downSampleLineData<
T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
>(
data: T[] | undefined,
maxDetails: number,
minX?: number,
maxX?: number
) {
if (!data || data.length < 10) {
return data;
): T[] {
if (!data) {
return [];
}
const width = chartWidth * window.devicePixelRatio;
if (data.length <= width) {
if (data.length <= maxDetails) {
return data;
}
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.floor((max - min) / width);
const step = Math.ceil((max - min) / Math.floor(maxDetails));
const frames = new Map<
number,
{
@@ -47,7 +48,7 @@ export function downSampleLineData(
}
// Convert frames back to points
const result: typeof data = [];
const result: T[] = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min

View File

@@ -805,7 +805,7 @@ export class HaChartBase extends LitElement {
sampling: undefined,
data: downSampleLineData(
data as LineSeriesOption["data"],
this.clientWidth,
this.clientWidth * window.devicePixelRatio,
minX,
maxX
),

View File

@@ -21,6 +21,7 @@ import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-icon-button";
import "../ha-input-helper-text";
import type {
PickerComboBoxItem,
PickerComboBoxSearchFn,
@@ -476,7 +477,6 @@ export class HaStatisticPicker extends LitElement {
.hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.helper=${this.helper}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
stat_power?: string;
name?: string;
included_in_stat?: string;
}
@@ -110,8 +109,6 @@ export interface DeviceConsumptionEnergyPreference {
export interface FlowFromGridSourceEnergyPreference {
// kWh meter
stat_energy_from: string;
// W meter
stat_power?: string;
// $ meter
stat_cost: string | null;
@@ -146,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference {
type: "solar";
stat_energy_from: string;
stat_power?: string;
config_entry_solar_forecast: string[] | null;
}
@@ -154,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference {
type: "battery";
stat_energy_from: string;
stat_energy_to: string;
stat_power?: string;
}
export interface GasSourceTypeEnergyPreference {
type: "gas";

View File

@@ -77,84 +77,80 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
if (!stateActive(this.stateObj)) {
return nothing;
}
const supportsMute = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_MUTE
);
const supportsSet = supportsFeature(
const supportsSliding = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_SET
);
const supportsStep = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
);
if (!supportsMute && !supportsSet && !supportsStep) {
return nothing;
}
return html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: nothing}
${supportsStep
? html` <ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>`
: nothing}
${supportsSet
? html`
${!supportsMute && !supportsStep
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) * 100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
${supportsStep
? html`
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
</div>
`;
return html`${(supportsFeature(
this.stateObj!,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(this.stateObj!)
? html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
) && !supportsSliding
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
${supportsSliding
? html`
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
</div>
`
: nothing}`;
}
protected _renderSourceControl() {

View File

@@ -15,7 +15,6 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { join } from "lit/directives/join";
import { keyed } from "lit/directives/keyed";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
@@ -33,6 +32,7 @@ import {
} from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-button-menu";
import "../../components/ha-dialog";
import "../../components/ha-dialog-header";
@@ -361,6 +361,8 @@ export class MoreInfoDialog extends LitElement {
);
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
const isRTL = computeRTL(this.hass);
return html`
<ha-dialog
open
@@ -394,17 +396,13 @@ export class MoreInfoDialog extends LitElement {
${breadcrumb.length > 0
? !__DEMO__ && isAdmin
? html`
<button
class="breadcrumb"
@click=${this._breadcrumbClick}
aria-label=${breadcrumb.join(" > ")}
>
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
<button class="breadcrumb" @click=${this._breadcrumbClick}>
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
</button>
`
: html`
<p class="breadcrumb">
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")}
</p>
`
: nothing}

View File

@@ -1,14 +1,10 @@
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-dialog-header";
import "../../components/ha-icon-button";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-wa-dialog";
import "../../components/ha-spinner";
import {
subscribeBackupEvents,
@@ -37,8 +33,6 @@ class DialogRestartWait extends LitElement {
private _backupEventsSubscription?: Promise<UnsubscribeFunc>;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: RestartWaitDialogParams): Promise<void> {
this._open = true;
this._loadBackupState();
@@ -49,9 +43,11 @@ class DialogRestartWait extends LitElement {
this._actionOnIdle = params.action;
}
private _dialogClosed(): void {
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
if (this._backupEventsSubscription) {
this._backupEventsSubscription.then((unsub) => {
unsub();
@@ -62,10 +58,6 @@ class DialogRestartWait extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog(): void {
this._dialog?.close();
}
private _getWaitMessage() {
switch (this._backupState) {
case "create_backup":
@@ -80,28 +72,17 @@ class DialogRestartWait extends LitElement {
}
protected render() {
if (!this._open) {
return nothing;
}
const waitMessage = this._getWaitMessage();
return html`
<ha-md-dialog
open
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this._title}
width="medium"
@closed=${this._dialogClosed}
.disableCancelAction=${true}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.cancel")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${this._title}> ${this._title} </span>
</ha-dialog-header>
<div slot="content" class="content">
<div class="content">
${this._error
? html`<ha-alert alert-type="error"
>${this.hass.localize("ui.dialogs.restart.error_backup_state", {
@@ -113,7 +94,7 @@ class DialogRestartWait extends LitElement {
${waitMessage}
`}
</div>
</ha-md-dialog>
</ha-wa-dialog>
`;
}
@@ -139,15 +120,9 @@ class DialogRestartWait extends LitElement {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
}
@media all and (min-width: 550px) {
ha-md-dialog {
min-width: 500px;
max-width: 500px;
}
}
.content {
display: flex;
flex-direction: column;

View File

@@ -1,5 +1,6 @@
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-resizable-bottom-sheet";
@@ -44,11 +45,27 @@ export default class HaAutomationSidebar extends LitElement {
@query("ha-resizable-bottom-sheet")
private _bottomSheetElement?: HaResizableBottomSheet;
@query(".handle")
private _handleElement?: HTMLDivElement;
private _resizeStartX = 0;
private _tinykeysUnsub?: () => void;
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("config") || changedProperties.has("narrow")) {
if (!this.config || this.narrow) {
this._tinykeysUnsub?.();
this._tinykeysUnsub = undefined;
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
this._tinykeysUnsub?.();
}
private _renderContent() {
@@ -170,6 +187,9 @@ export default class HaAutomationSidebar extends LitElement {
class="handle ${this._resizing ? "resizing" : ""}"
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleMouseDown}
@focus=${this._startKeyboardResizing}
@blur=${this._stopKeyboardResizing}
tabindex="0"
>
<div class="indicator ${this._resizing ? "" : "hidden"}"></div>
</div>
@@ -288,6 +308,44 @@ export default class HaAutomationSidebar extends LitElement {
document.removeEventListener("touchcancel", this._endResizing);
}
private _startKeyboardResizing = (ev: KeyboardEvent) => {
ev.stopPropagation();
this._resizing = true;
this._resizeStartX = 0;
this._tinykeysUnsub = tinykeys(this._handleElement!, {
ArrowLeft: this._increaseSize,
ArrowRight: this._decreaseSize,
});
};
private _stopKeyboardResizing = (ev: KeyboardEvent) => {
ev.stopPropagation();
this._resizing = false;
fireEvent(this, "sidebar-resizing-stopped");
this._tinykeysUnsub?.();
this._tinykeysUnsub = undefined;
};
private _increaseSize = (ev: KeyboardEvent) => {
ev.stopPropagation();
this._resizeStartX -= computeRTL(this.hass) ? 10 : -10;
this._keyboardResize();
};
private _decreaseSize = (ev: KeyboardEvent) => {
ev.stopPropagation();
this._resizeStartX += computeRTL(this.hass) ? 10 : -10;
this._keyboardResize();
};
private _keyboardResize() {
fireEvent(this, "sidebar-resized", {
deltaInPx: this._resizeStartX,
});
}
static styles = css`
:host {
z-index: 6;
@@ -342,6 +400,10 @@ export default class HaAutomationSidebar extends LitElement {
transform: scale3d(0, 1, 1);
opacity: 0;
}
.handle:focus-visible {
outline: none;
}
`;
}

View File

@@ -1,14 +1,11 @@
import { mdiCalendarSync, mdiClose, mdiGestureTap } from "@mdi/js";
import { mdiCalendarSync, mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
@@ -24,92 +21,80 @@ class DialogNewBackup extends LitElement implements HassDialog {
@state() private _params?: NewBackupDialogParams;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(params: NewBackupDialogParams): void {
this._opened = true;
this._params = params;
}
public closeDialog() {
this._dialog?.close();
this._opened = false;
return true;
}
private _dialogClosed() {
if (this._params!.cancel) {
this._params!.cancel();
if (this._params?.cancel) {
this._params.cancel();
}
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._opened || !this._params) {
if (!this._params) {
return nothing;
}
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize("ui.panel.config.backup.dialogs.new.title")}
</span>
</ha-dialog-header>
<div slot="content">
<ha-md-list
innerRole="listbox"
itemRoles="option"
.innerAriaLabel=${this.hass.localize(
"ui.panel.config.backup.dialogs.new.options"
)}
rootTabbable
dialogInitialFocus
<ha-wa-dialog
.hass=${this.hass}
.open=${this._opened}
header-title=${this.hass.localize(
"ui.panel.config.backup.dialogs.new.title"
)}
@closed=${this._dialogClosed}
>
<ha-md-list
innerRole="listbox"
itemRoles="option"
.innerAriaLabel=${this.hass.localize(
"ui.panel.config.backup.dialogs.new.options"
)}
rootTabbable
>
<ha-md-list-item
@click=${this._automatic}
type="button"
.disabled=${!this._params.config.create_backup.password}
>
<ha-md-list-item
@click=${this._automatic}
type="button"
.disabled=${!this._params.config.create_backup.password}
>
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.automatic.title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.automatic.description"
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item @click=${this._manual} type="button">
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.manual.title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.manual.description"
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</div>
</ha-md-dialog>
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.automatic.title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.automatic.description"
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
<ha-md-list-item @click=${this._manual} type="button">
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
<span slot="headline">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.manual.title"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.panel.config.backup.dialogs.new.manual.description"
)}
</span>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
</ha-md-list>
</ha-wa-dialog>
`;
}
@@ -128,24 +113,13 @@ class DialogNewBackup extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
max-width: 500px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
ha-md-list {
background: none;
}
ha-md-list-item {
}
ha-icon-next {
width: 24px;
}

View File

@@ -18,7 +18,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-battery-settings")
export class DialogEnergyBatterySettings
@@ -33,14 +32,10 @@ export class DialogEnergyBatterySettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsBatteryDialogParams
): Promise<void> {
@@ -51,9 +46,6 @@ export class DialogEnergyBatterySettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
const allSources: string[] = [];
this._params.battery_sources.forEach((entry) => {
allSources.push(entry.stat_energy_from);
@@ -64,9 +56,6 @@ export class DialogEnergyBatterySettings
id !== this._source?.stat_energy_from &&
id !== this._source?.stat_energy_to
);
this._excludeListPower = this._params.battery_sources
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._source?.stat_power) as string[];
}
public closeDialog() {
@@ -83,6 +72,8 @@ export class DialogEnergyBatterySettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -94,6 +85,12 @@ export class DialogEnergyBatterySettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.battery.dialog.entity_para",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -108,10 +105,6 @@ export class DialogEnergyBatterySettings
this._source.stat_energy_from,
]}
@value-changed=${this._statisticToChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_into",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
@@ -128,25 +121,6 @@ export class DialogEnergyBatterySettings
this._source.stat_energy_to,
]}
@value-changed=${this._statisticFromChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_out",
{ unit: this._energy_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._source.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.power_helper",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-button
@@ -176,10 +150,6 @@ export class DialogEnergyBatterySettings
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private _powerChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_power: ev.detail.value };
}
private async _save() {
try {
await this._params!.saveCallback(this._source!);
@@ -198,11 +168,7 @@ export class DialogEnergyBatterySettings
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-4);
}
ha-statistic-picker:last-of-type {
margin-bottom: 0;
width: 100%;
}
`,
];

View File

@@ -21,7 +21,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-device-settings")
export class DialogEnergyDeviceSettings
@@ -36,14 +35,10 @@ export class DialogEnergyDeviceSettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
public async showDialog(
@@ -55,15 +50,9 @@ export class DialogEnergyDeviceSettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeList = this._params.device_consumptions
.map((entry) => entry.stat_consumption)
.filter((id) => id !== this._device?.stat_consumption);
this._excludeListPower = this._params.device_consumptions
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._device?.stat_power) as string[];
}
private _computePossibleParents() {
@@ -104,6 +93,8 @@ export class DialogEnergyDeviceSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -117,6 +108,12 @@ export class DialogEnergyDeviceSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -128,28 +125,9 @@ export class DialogEnergyDeviceSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._device?.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.device_consumption_power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.display_name"
@@ -232,20 +210,6 @@ export class DialogEnergyDeviceSettings
this._computePossibleParents();
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
if (!this._device) {
return;
}
const newDevice = {
...this._device,
stat_power: ev.detail.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.stat_power) {
delete newDevice.stat_power;
}
this._device = newDevice;
}
private _nameChanged(ev) {
const newDevice = {
...this._device!,
@@ -281,19 +245,15 @@ export class DialogEnergyDeviceSettings
return [
haStyleDialog,
css`
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-2);
}
ha-statistic-picker {
width: 100%;
}
ha-select {
margin-top: var(--ha-space-4);
margin-top: 16px;
width: 100%;
}
ha-textfield {
margin-top: var(--ha-space-4);
margin-top: 16px;
width: 100%;
}
`,

View File

@@ -31,7 +31,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsGridFlowDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-grid-flow-settings")
export class DialogEnergyGridFlowSettings
@@ -52,14 +51,10 @@ export class DialogEnergyGridFlowSettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsGridFlowDialogParams
): Promise<void> {
@@ -85,9 +80,6 @@ export class DialogEnergyGridFlowSettings
? "stat_energy_from"
: "stat_energy_to"
];
const initialSourceIdPower = (
this._source as FlowFromGridSourceEnergyPreference
).stat_power;
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
@@ -97,9 +89,6 @@ export class DialogEnergyGridFlowSettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeList = [
...(this._params.grid_source?.flow_from?.map(
@@ -109,12 +98,6 @@ export class DialogEnergyGridFlowSettings
(entry) => entry.stat_energy_to
) || []),
].filter((id) => id !== initialSourceId);
this._excludeListPower = [
...(this._params.grid_source?.flow_from?.map(
(entry) => entry.stat_power
) || []),
].filter((id) => id && id !== initialSourceIdPower) as string[];
}
public closeDialog() {
@@ -132,6 +115,8 @@ export class DialogEnergyGridFlowSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
const unitPriceSensor = this._pickedDisplayUnit
? `${this.hass.config.currency}/${this._pickedDisplayUnit}`
: undefined;
@@ -165,11 +150,19 @@ export class DialogEnergyGridFlowSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
)}
</p>
<div>
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
)}
</p>
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
{ unit: pickableUnit }
)}
</p>
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -185,33 +178,9 @@ export class DialogEnergyGridFlowSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
${this._params.direction === "from"
? html`
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${(this._source as FlowFromGridSourceEnergyPreference)
.stat_power}
.label=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.power_stat`
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.from.power_helper`,
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
`
: nothing}
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_para`
@@ -371,13 +340,6 @@ export class DialogEnergyGridFlowSettings
};
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = {
...this._source!,
stat_power: ev.detail.value,
};
}
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) {
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
@@ -418,10 +380,6 @@ export class DialogEnergyGridFlowSettings
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin: var(--ha-space-4) 0;
}
ha-formfield {
display: block;
}

View File

@@ -28,7 +28,6 @@ import { brandsUrl } from "../../../../util/brands-url";
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-solar-settings")
export class DialogEnergySolarSettings
@@ -47,14 +46,10 @@ export class DialogEnergySolarSettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsSolarDialogParams
): Promise<void> {
@@ -67,15 +62,9 @@ export class DialogEnergySolarSettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeList = this._params.solar_sources
.map((entry) => entry.stat_energy_from)
.filter((id) => id !== this._source?.stat_energy_from);
this._excludeListPower = this._params.solar_sources
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._source?.stat_power) as string[];
}
public closeDialog() {
@@ -92,6 +81,8 @@ export class DialogEnergySolarSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -103,6 +94,12 @@ export class DialogEnergySolarSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -114,28 +111,9 @@ export class DialogEnergySolarSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._source.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<h3>
${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_forecast"
@@ -289,10 +267,6 @@ export class DialogEnergySolarSettings
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_power: ev.detail.value };
}
private async _save() {
try {
if (!this._forecast) {
@@ -313,10 +287,6 @@ export class DialogEnergySolarSettings
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-4);
}
img {
height: 24px;
margin-right: 16px;

View File

@@ -128,11 +128,10 @@ class ZHAAddDevicesPage extends LitElement {
this.hass,
"/integrations/zha#adding-devices"
)}
>
${this.hass.localize(
>${this.hass.localize(
"ui.panel.config.zha.add_device_page.pairing_mode_link"
)}
</a>
)}</a
>
`,
}
)}

View File

@@ -110,191 +110,200 @@ class ZHAConfigDashboard extends LitElement {
back-path="/config/integrations"
has-fab
>
<ha-card class="content network-status">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="card-content">
<div class="heading">
<div class="icon">
<ha-svg-icon
.path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle}
class=${deviceOnline ? "online" : "offline"}
></ha-svg-icon>
</div>
<div class="details">
ZHA
${this.hass.localize(
"ui.panel.config.zha.configuration_page.status_title"
)}:
${this.hass.localize(
`ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}`
)}<br />
<small>
<div class="container">
<ha-card class="content network-status">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="card-content">
<div class="heading">
<div class="icon">
<ha-svg-icon
.path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle}
class=${deviceOnline ? "online" : "offline"}
></ha-svg-icon>
</div>
<div class="details">
ZHA
${this.hass.localize(
"ui.panel.config.zha.configuration_page.devices",
{ count: this._totalDevices }
)}
</small>
<small class="offline">
${this._offlineDevices > 0
? html`(${this.hass.localize(
"ui.panel.config.zha.configuration_page.devices_offline",
{ count: this._offlineDevices }
)})`
: nothing}
</small>
"ui.panel.config.zha.configuration_page.status_title"
)}:
${this.hass.localize(
`ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}`
)}<br />
<small>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.devices",
{ count: this._totalDevices }
)}
</small>
<small class="offline">
${this._offlineDevices > 0
? html`(${this.hass.localize(
"ui.panel.config.zha.configuration_page.devices_offline",
{ count: this._offlineDevices }
)})`
: nothing}
</small>
</div>
</div>
</div>
</div>
${this.configEntryId
? html`<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
appearance="plain"
size="small"
>
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</div>`
: ""}
</ha-card>
<ha-card
class="network-settings"
header=${this.hass.localize(
"ui.panel.config.zha.configuration_page.network_settings_title"
)}
>
${this._networkSettings
? html`<div class="card-content">
<ha-settings-row>
<span slot="description">PAN ID</span>
<span slot="heading"
>${this._networkSettings.settings.network_info.pan_id}</span
${this.configEntryId
? html`<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
appearance="plain"
size="small"
>
</ha-settings-row>
<ha-settings-row>
<span slot="heading"
>${this._networkSettings.settings.network_info
.extended_pan_id}</span
${this.hass.localize(
"ui.panel.config.devices.caption"
)}</ha-button
>
<span slot="description">Extended PAN ID</span>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Channel</span>
<span slot="heading"
>${this._networkSettings.settings.network_info
.channel}</span
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.zha.configuration_page.change_channel"
)}
.path=${mdiPencil}
@click=${this._showChannelMigrationDialog}
${this.hass.localize(
"ui.panel.config.entities.caption"
)}</ha-button
>
</ha-icon-button>
</ha-settings-row>
</div>`
: ""}
</ha-card>
<ha-card
class="network-settings"
header=${this.hass.localize(
"ui.panel.config.zha.configuration_page.network_settings_title"
)}
>
${this._networkSettings
? html`<div class="card-content">
<ha-settings-row>
<span slot="description">PAN ID</span>
<span slot="heading"
>${this._networkSettings.settings.network_info
.pan_id}</span
>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Coordinator IEEE</span>
<span slot="heading"
>${this._networkSettings.settings.node_info.ieee}</span
>
</ha-settings-row>
<ha-settings-row>
<span slot="heading"
>${this._networkSettings.settings.network_info
.extended_pan_id}</span
>
<span slot="description">Extended PAN ID</span>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Radio type</span>
<span slot="heading"
>${this._networkSettings.radio_type}</span
>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Channel</span>
<span slot="heading"
>${this._networkSettings.settings.network_info
.channel}</span
>
<ha-settings-row>
<span slot="description">Serial port</span>
<span slot="heading"
>${this._networkSettings.device.path}</span
>
</ha-settings-row>
${this._networkSettings.device.baudrate &&
!this._networkSettings.device.path.startsWith("socket://")
? html`
<ha-settings-row>
<span slot="description">Baudrate</span>
<span slot="heading"
>${this._networkSettings.device.baudrate}</span
>
</ha-settings-row>
`
: ""}
</div>`
: ""}
<div class="card-actions">
<ha-progress-button
appearance="plain"
@click=${this._createAndDownloadBackup}
.progress=${this._generatingBackup}
.disabled=${!this._networkSettings || this._generatingBackup}
>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.download_backup"
)}
</ha-progress-button>
<ha-button variant="danger" @click=${this._openOptionFlow}>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.migrate_radio"
)}
</ha-button>
</div>
</ha-card>
${this._configuration
? Object.entries(this._configuration.schemas).map(
([section, schema]) =>
html`<ha-card
header=${this.hass.localize(
`component.zha.config_panel.${section}.title`
)}
>
<div class="card-content">
<ha-form
.hass=${this.hass}
.schema=${schema}
.data=${this._configuration!.data[section]}
@value-changed=${this._dataChanged}
.section=${section}
.computeLabel=${this._computeLabelCallback(
this.hass.localize,
section
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.zha.configuration_page.change_channel"
)}
></ha-form>
</div>
</ha-card>`
)
: ""}
<ha-card>
<div class="card-actions">
<ha-button @click=${this._updateConfiguration}>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.update_button"
)}
</ha-button>
</div>
</ha-card>
.path=${mdiPencil}
@click=${this._showChannelMigrationDialog}
>
</ha-icon-button>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Coordinator IEEE</span>
<span slot="heading"
>${this._networkSettings.settings.node_info.ieee}</span
>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Radio type</span>
<span slot="heading"
>${this._networkSettings.radio_type}</span
>
</ha-settings-row>
<ha-settings-row>
<span slot="description">Serial port</span>
<span slot="heading"
>${this._networkSettings.device.path}</span
>
</ha-settings-row>
${this._networkSettings.device.baudrate &&
!this._networkSettings.device.path.startsWith("socket://")
? html`
<ha-settings-row>
<span slot="description">Baudrate</span>
<span slot="heading"
>${this._networkSettings.device.baudrate}</span
>
</ha-settings-row>
`
: nothing}
</div>`
: nothing}
<div class="card-actions">
<ha-progress-button
appearance="plain"
@click=${this._createAndDownloadBackup}
.progress=${this._generatingBackup}
.disabled=${!this._networkSettings || this._generatingBackup}
>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.download_backup"
)}
</ha-progress-button>
<ha-button
appearance="filled"
variant="brand"
@click=${this._openOptionFlow}
>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.migrate_radio"
)}
</ha-button>
</div>
</ha-card>
${this._configuration
? Object.entries(this._configuration.schemas).map(
([section, schema]) =>
html`<ha-card
header=${this.hass.localize(
`component.zha.config_panel.${section}.title`
)}
>
<div class="card-content">
<ha-form
.hass=${this.hass}
.schema=${schema}
.data=${this._configuration!.data[section]}
@value-changed=${this._dataChanged}
.section=${section}
.computeLabel=${this._computeLabelCallback(
this.hass.localize,
section
)}
></ha-form>
</div>
<div class="card-actions">
<ha-button
appearance="filled"
variant="brand"
@click=${this._updateConfiguration}
>
${this.hass.localize(
"ui.panel.config.zha.configuration_page.update_button"
)}
</ha-button>
</div>
</ha-card>`
)
: nothing}
</div>
<a href="/config/zha/add" slot="fab">
<ha-fab
@@ -489,6 +498,10 @@ class ZHAConfigDashboard extends LitElement {
.network-status .offline {
color: var(--error-color, var(--error-color));
}
.container {
padding: 8px 16px 16px;
}
`,
];
}

View File

@@ -9,9 +9,9 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import "../../../components/ha-badge";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
@@ -189,7 +189,11 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
</state-display>
`;
const name = this._config.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const showState = this._config.show_state;
const showName = this._config.show_name;

View File

@@ -1,3 +1,4 @@
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import type { ActionConfig } from "../../../data/lovelace/config/action";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LegacyStateFilter } from "../common/evaluate-filter";
@@ -31,7 +32,7 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
export interface EntityBadgeConfig extends LovelaceBadgeConfig {
type: "entity";
entity?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
icon?: string;
color?: string;
show_name?: boolean;

View File

@@ -43,6 +43,8 @@ class HuiHistoryChartCardFeature
@state() private _coordinates?: [number, number][];
@state() private _yAxisOrigin?: number;
private _interval?: number;
static getStubConfig(): TrendGraphCardFeatureConfig {
@@ -105,7 +107,10 @@ class HuiHistoryChartCardFeature
`;
}
return html`
<hui-graph-base .coordinates=${this._coordinates}></hui-graph-base>
<hui-graph-base
.coordinates=${this._coordinates}
.yAxisOrigin=${this._yAxisOrigin}
></hui-graph-base>
`;
}
@@ -123,14 +128,15 @@ class HuiHistoryChartCardFeature
return subscribeHistoryStatesTimeWindow(
this.hass!,
(historyStates) => {
this._coordinates =
const { points, yAxisOrigin } =
coordinatesMinimalResponseCompressedState(
historyStates[this.context!.entity_id!],
hourToShow,
500,
2,
undefined
) || [];
this.clientWidth,
this.clientHeight,
this.clientWidth / 5 // sample to 1 point per 5 pixels
);
this._coordinates = points;
this._yAxisOrigin = yAxisOrigin;
},
hourToShow,
[this.context!.entity_id!]

View File

@@ -28,6 +28,7 @@ import {
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import type { HomeAssistant } from "../../../types";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard } from "../types";
@@ -232,12 +233,16 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
const defaultCode = this._entry?.options?.alarm_control_panel?.default_code;
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html`
<ha-card>
<h1 class="card-header">
${this._config.name ||
stateObj.attributes.friendly_name ||
stateLabel}
${name}
<ha-assist-chip
filled
style=${styleMap({

View File

@@ -8,7 +8,6 @@ import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
stateColorBrightness,
stateColorCss,
@@ -27,6 +26,7 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { isUnavailableState } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -125,7 +125,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
? this._config.attribute in stateObj.attributes
: !isUnavailableState(stateObj.state);
const name = this._config.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const colored = stateObj && this._getStateColor(stateObj, this._config);

View File

@@ -2,11 +2,10 @@ import type { HassEntity } from "home-assistant-js-websocket/dist/types";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { getNumberFormatOptions } from "../../../common/number/format_number";
import "../../../components/ha-card";
@@ -15,6 +14,7 @@ import { UNAVAILABLE } from "../../../data/entity";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction, hasAnyAction } from "../common/has-action";
@@ -126,13 +126,19 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config.name ?? computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
// Use `stateObj.state` as value to keep formatting (e.g trailing zeros)
// for consistent value display across gauge, entity, entity-row, etc.
return html`
<ha-card
class=${classMap({ action: hasAnyAction(this._config) })}
class=${classMap({
action: hasAnyAction(this._config),
})}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config.hold_action),

View File

@@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -15,6 +14,7 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
@@ -133,7 +133,11 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config!.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const color = stateColorCss(stateObj);

View File

@@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorBrightness } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -18,6 +17,7 @@ import { lightSupportsBrightness } from "../../../data/light";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -92,7 +92,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
((stateObj.attributes.brightness || 0) / 255) * 100
);
const name = this._config.name ?? computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html`
<ha-card>

View File

@@ -12,7 +12,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { extractColors } from "../../../common/image/extract_color";
import { stateActive } from "../../../common/entity/state_active";
@@ -36,6 +35,7 @@ import {
mediaPlayerPlayMedia,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-marquee";
@@ -242,8 +242,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
.hass=${this.hass}
></ha-state-icon>
<div>
${this._config!.name ||
computeStateName(this.hass!.states[this._config!.entity])}
${computeLovelaceEntityName(
this.hass,
this.hass!.states[this._config!.entity],
this._config.name
)}
</div>
</div>
<div>

View File

@@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import type { CameraEntity } from "../../../data/camera";
import type { ImageEntity } from "../../../data/image";
@@ -14,6 +13,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { PersonEntity } from "../../../data/person";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -126,7 +126,11 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const entityState = this.hass.formatEntityState(stateObj);
let footer: TemplateResult | string = "";

View File

@@ -11,11 +11,11 @@ import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { batteryLevelIcon } from "../../../common/entity/battery_icon";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -119,7 +119,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
style="background-image:url(${stateObj.attributes.entity_picture})"
>
<div class="header">
${this._config.name || computeStateName(stateObj)}
${computeLovelaceEntityName(this.hass, stateObj, this._config.name)}
</div>
</div>
<div class="content">

View File

@@ -7,7 +7,6 @@ import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -16,6 +15,7 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
@@ -132,7 +132,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
const domain = computeDomain(stateObj.entity_id);
const name = this._config!.name || computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const color = stateColorCss(stateObj);

View File

@@ -9,7 +9,6 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
@@ -26,6 +25,7 @@ import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -47,11 +47,6 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
return supportsIconAction ? "toggle" : "none";
};
export const DEFAULT_NAME = [
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
@customElement("hui-tile-card")
export class HuiTileCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@@ -260,12 +255,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const contentClasses = { vertical: Boolean(this._config.vertical) };
const nameConfig = this._config.name;
const nameDisplay =
typeof nameConfig === "string"
? nameConfig
: this.hass.formatEntityName(stateObj, nameConfig || DEFAULT_NAME);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
@@ -278,7 +272,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${nameDisplay}
.name=${name}
>
</state-display>
`;
@@ -337,7 +331,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info id="info">
<span slot="primary" class="primary">${nameDisplay}</span>
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}

View File

@@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
@@ -27,6 +26,7 @@ import {
} from "../../../data/weather";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -229,7 +229,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return html`
<ha-card class="unavailable" @click=${this._handleAction}>
${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", {
entity: `${computeStateName(stateObj)} (${this._config.entity})`,
entity: `${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} (${this._config.entity})`,
})}
</ha-card>
`;
@@ -260,7 +260,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
const dayNight = forecastData?.type === "twice_daily";
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
const name = this._config.name ?? computeStateName(stateObj);
const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html`
<ha-card

View File

@@ -40,7 +40,7 @@ export type AlarmPanelCardConfigState =
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
states?: AlarmPanelCardConfigState[];
theme?: string;
}
@@ -63,6 +63,9 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig {
}
export interface EntityCardConfig extends LovelaceCardConfig {
entity: string;
name?: string | EntityNameItem | EntityNameItem[];
icon?: string;
attribute?: string;
unit?: string;
theme?: string;
@@ -258,7 +261,7 @@ export interface GaugeSegment {
export interface GaugeCardConfig extends LovelaceCardConfig {
entity: string;
attribute?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
unit?: string;
min?: number;
max?: number;
@@ -271,12 +274,14 @@ export interface GaugeCardConfig extends LovelaceCardConfig {
double_tap_action?: ActionConfig;
}
export interface ConfigEntity extends EntityConfig {
export interface ActionsConfig {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}
export interface ConfigEntity extends EntityConfig, ActionsConfig {}
export interface PictureGlanceEntityConfig extends ConfigEntity {
show_state?: boolean;
attribute?: string;
@@ -306,7 +311,7 @@ export interface GlanceCardConfig extends LovelaceCardConfig {
export interface HumidifierCardConfig extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current_as_primary?: boolean;
features?: LovelaceCardFeatureConfig[];
}
@@ -322,7 +327,7 @@ export interface IframeCardConfig extends LovelaceCardConfig {
export interface LightCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
theme?: string;
icon?: string;
tap_action?: ActionConfig;
@@ -394,6 +399,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
export interface MediaControlCardConfig extends LovelaceCardConfig {
entity: string;
name?: string | EntityNameItem | EntityNameItem[];
theme?: string;
}
@@ -469,7 +475,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
export interface PictureEntityCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
@@ -509,14 +515,14 @@ export interface PlantAttributeTarget extends EventTarget {
}
export interface PlantStatusCardConfig extends LovelaceCardConfig {
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
entity: string;
theme?: string;
}
export interface SensorCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
icon?: string;
graph?: string;
unit?: string;
@@ -552,14 +558,14 @@ export interface GridCardConfig extends StackCardConfig {
export interface ThermostatCardConfig extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current_as_primary?: boolean;
features?: LovelaceCardFeatureConfig[];
}
export interface WeatherForecastCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current?: boolean;
show_forecast?: boolean;
forecast_type?: ForecastType;

View File

@@ -0,0 +1,23 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types";
/**
* Computes the display name for an entity in Lovelace (cards and badges).
*
* @param hass - The Home Assistant instance
* @param stateObj - The entity state object
* @param nameConfig - The name configuration (string for override, or EntityNameItem[] for structured naming)
* @returns The computed entity name
*/
export const computeLovelaceEntityName = (
hass: HomeAssistant,
stateObj: HassEntity,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
): string =>
typeof nameConfig === "string"
? nameConfig
: hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME);

View File

@@ -1,134 +1,85 @@
import { strokeWidth } from "../../../../data/graph";
import { downSampleLineData } from "../../../../components/chart/down-sample";
import type { EntityHistoryState } from "../../../../data/history";
const average = (items: any[]): number =>
items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / items.length;
const lastValue = (items: any[]): number =>
parseFloat(items[items.length - 1].state) || 0;
const calcPoints = (
history: any,
hours: number,
history: [number, number][],
width: number,
detail: number,
min: number,
max: number
): [number, number][] => {
const coords = [] as [number, number][];
const height = 80;
let yRatio = (max - min) / height;
yRatio = yRatio !== 0 ? yRatio : height;
let xRatio = width / (hours - (detail === 1 ? 1 : 0));
xRatio = isFinite(xRatio) ? xRatio : width;
let first = history.filter(Boolean)[0];
if (detail > 1) {
first = first.filter(Boolean)[0];
}
let last = [average(first), lastValue(first)];
const getY = (value: number): number =>
height + strokeWidth / 2 - (value - min) / yRatio;
const getCoords = (item: any[], i: number, offset = 0, depth = 1) => {
if (depth > 1 && item) {
return item.forEach((subItem, index) =>
getCoords(subItem, i, index, depth - 1)
);
height: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
let yAxisOrigin = height;
let minY = limits?.minY ?? history[0][1];
let maxY = limits?.maxY ?? history[0][1];
const minX = limits?.minX ?? history[0][0];
const maxX = limits?.maxX ?? history[history.length - 1][0];
history.forEach(([_, stateValue]) => {
if (stateValue < minY) {
minY = stateValue;
} else if (stateValue > maxY) {
maxY = stateValue;
}
const x = xRatio * (i + offset / 6);
if (item) {
last = [average(item), lastValue(item)];
}
const y = getY(item ? last[0] : last[1]);
return coords.push([x, y]);
};
for (let i = 0; i < history.length; i += 1) {
getCoords(history[i], i, 0, detail);
});
const rangeY = maxY - minY || minY * 0.1;
if (maxY < 0) {
// all values are negative
// add margin
maxY += rangeY * 0.1;
maxY = Math.min(0, maxY);
yAxisOrigin = 0;
} else if (minY < 0) {
// some values are negative
yAxisOrigin = (maxY / (maxY - minY || 1)) * height;
} else {
// all values are positive
// add margin
minY -= rangeY * 0.1;
minY = Math.max(0, minY);
}
coords.push([width, getY(last[1])]);
return coords;
const yDenom = maxY - minY || 1;
const xDenom = maxX - minX || 1;
const points: [number, number][] = history.map((point) => {
const x = ((point[0] - minX) / xDenom) * width;
const y = height - ((point[1] - minY) / yDenom) * height;
return [x, y];
});
points.push([width, points[points.length - 1][1]]);
return { points, yAxisOrigin };
};
export const coordinates = (
history: any,
hours: number,
history: [number, number][],
width: number,
detail: number,
limits?: { min?: number; max?: number }
): [number, number][] | undefined => {
history.forEach((item) => {
item.state = Number(item.state);
});
history = history.filter((item) => !Number.isNaN(item.state));
height: number,
maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
history = history.filter((item) => !Number.isNaN(item[1]));
const min =
limits?.min !== undefined
? limits.min
: Math.min(...history.map((item) => item.state));
const max =
limits?.max !== undefined
? limits.max
: Math.max(...history.map((item) => item.state));
const now = new Date().getTime();
const reduce = (res, item, point) => {
const age = now - new Date(item.last_changed).getTime();
let key = Math.abs(age / (1000 * 3600) - hours);
if (point) {
key = (key - Math.floor(key)) * 60;
key = Number((Math.round(key / 10) * 10).toString()[0]);
} else {
key = Math.floor(key);
}
if (!res[key]) {
res[key] = [];
}
res[key].push(item);
return res;
};
history = history.reduce((res, item) => reduce(res, item, false), []);
if (detail > 1) {
history = history.map((entry) =>
entry.reduce((res, item) => reduce(res, item, true), [])
);
}
if (!history.length) {
return undefined;
}
return calcPoints(history, hours, width, detail, min, max);
const sampledData: [number, number][] = downSampleLineData(
history,
maxDetails,
limits?.minX,
limits?.maxX
);
return calcPoints(sampledData, width, height, limits);
};
interface NumericEntityHistoryState {
state: number;
last_changed: number;
}
export const coordinatesMinimalResponseCompressedState = (
history: EntityHistoryState[],
hours: number,
history: EntityHistoryState[] | undefined,
width: number,
detail: number,
limits?: { min?: number; max?: number }
): [number, number][] | undefined => {
if (!history) {
return undefined;
height: number,
maxDetails: number,
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
) => {
if (!history?.length) {
return { points: [], yAxisOrigin: 0 };
}
const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({
state: Number(item.s),
const mappedHistory: [number, number][] = history.map((item) => [
// With minimal response and compressed state, we don't have last_changed,
// so we use last_updated since its always the same as last_changed since
// we already filtered out states that are the same.
last_changed: item.lu * 1000,
}));
return coordinates(numericHistory, hours, width, detail, limits);
item.lu * 1000,
Number(item.s),
]);
return coordinates(mappedHistory, width, height, maxDetails, limits);
};

View File

@@ -1,11 +1,11 @@
import type { ActionConfig } from "../../../data/lovelace/config/action";
import type { ConfigEntity } from "../cards/types";
import type { ActionsConfig } from "../cards/types";
export function hasAction(config?: ActionConfig): boolean {
return config !== undefined && config.action !== "none";
}
export function hasAnyAction(config: ConfigEntity): boolean {
export function hasAnyAction(config: ActionsConfig): boolean {
return (
!config.tap_action ||
hasAction(config.tap_action) ||

View File

@@ -6,20 +6,26 @@ import { getPath } from "../common/graph/get-path";
@customElement("hui-graph-base")
export class HuiGraphBase extends LitElement {
@property() public coordinates?: any;
@property({ attribute: false }) public coordinates?: number[][];
@property({ attribute: "y-axis-origin", type: Number })
public yAxisOrigin?: number;
@state() private _path?: string;
protected render(): TemplateResult {
const width = this.clientWidth || 500;
const height = this.clientHeight || width / 5;
const yAxisOrigin = this.yAxisOrigin ?? height;
return html`
${this._path
? svg`<svg width="100%" height="100%" viewBox="0 0 500 100" preserveAspectRatio="none">
? svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<g>
<mask id="fill">
<path
class='fill'
fill='white'
d="${this._path} L 500, 100 L 0, 100 z"
d="${this._path} L ${width}, ${yAxisOrigin} L 0, ${yAxisOrigin} z"
/>
</mask>
<rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect>
@@ -38,7 +44,7 @@ export class HuiGraphBase extends LitElement {
<rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect>
</g>
</svg>`
: svg`<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>`}
: svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}"></svg>`}
`;
}

View File

@@ -1,32 +1,34 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { array, assert, assign, object, optional, string } from "superstruct";
import type { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { ALARM_MODES } from "../../../../data/alarm_control_panel";
import type { HomeAssistant } from "../../../../types";
import {
ALARM_MODE_STATE_MAP,
DEFAULT_STATES,
filterSupportedAlarmStates,
} from "../../cards/hui-alarm-panel-card";
import type {
AlarmPanelCardConfig,
AlarmPanelCardConfigState,
} from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import {
DEFAULT_STATES,
ALARM_MODE_STATE_MAP,
filterSupportedAlarmStates,
} from "../../cards/hui-alarm-panel-card";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { ALARM_MODES } from "../../../../data/alarm_control_panel";
import { entityNameStruct } from "../structs/entity-name-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
states: optional(array()),
theme: optional(string()),
})
@@ -61,13 +63,15 @@ export class HuiAlarmPanelCardEditor
selector: { entity: { domain: "alarm_control_panel" } },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "theme", selector: { theme: {} } },
{
name: "states",
selector: {

View File

@@ -13,6 +13,7 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -31,6 +32,7 @@ import type { LovelaceBadgeEditor } from "../../types";
import "../hui-sub-element-editor";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@@ -39,7 +41,7 @@ const badgeConfigStruct = assign(
object({
entity: optional(string()),
display_type: optional(enums(DISPLAY_TYPES)),
name: optional(string()),
name: optional(entityNameStruct),
icon: optional(string()),
state_content: optional(union([string(), array(string())])),
color: optional(string()),
@@ -81,16 +83,19 @@ export class HuiEntityBadgeEditor
flatten: true,
iconPath: mdiTextShort,
schema: [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{
name: "name",
selector: {
text: {},
},
},
{
name: "color",
selector: {

View File

@@ -1,16 +1,17 @@
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type { EntityCardConfig } from "../../cards/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
import type { LovelaceConfigForm } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
const struct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
icon: optional(string()),
attribute: optional(string()),
unit: optional(string()),
@@ -22,11 +23,19 @@ const struct = assign(
const SCHEMA = [
{ name: "entity", required: true, selector: { entity: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
@@ -54,7 +63,7 @@ const SCHEMA = [
const entityCardConfigForm: LovelaceConfigForm = {
schema: SCHEMA,
assertConfig: (config: EntityCardConfig) => assert(config, struct),
assertConfig: (config) => assert(config, struct),
computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => {
if (schema.name === "theme") {
return `${localize(

View File

@@ -14,8 +14,10 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card";
import type { GaugeCardConfig } from "../../cards/types";
@@ -23,7 +25,7 @@ import type { UiAction } from "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes";
import { entityNameStruct } from "../structs/entity-name-struct";
const TAP_ACTIONS: UiAction[] = [
"more-info",
@@ -43,7 +45,7 @@ const gaugeSegmentStruct = object({
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
name: optional(string()),
name: optional(entityNameStruct),
entity: optional(string()),
attribute: optional(string()),
unit: optional(string()),
@@ -98,13 +100,15 @@ export class HuiGaugeCardEditor
},
},
{
name: "",
type: "grid",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "unit", selector: { text: {} } },
],
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "unit", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "",

View File

@@ -14,6 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -29,6 +30,7 @@ import type {
import type { HumidifierCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@@ -43,7 +45,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current_as_primary: optional(boolean()),
features: optional(array(any())),
@@ -56,13 +58,19 @@ const SCHEMA = [
required: true,
selector: { entity: { domain: "humidifier" } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
schema: [{ name: "theme", selector: { theme: {} } }],
},
{
name: "show_current_as_primary",

View File

@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -11,12 +12,13 @@ import type { LightCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
name: optional(string()),
name: optional(entityNameStruct),
entity: optional(string()),
theme: optional(string()),
icon: optional(string()),
@@ -32,11 +34,19 @@ const SCHEMA = [
required: true,
selector: { entity: { domain: "light" } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {

View File

@@ -2,23 +2,45 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-theme-picker";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { MediaControlCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EditorTarget, EntitiesEditorEvent } from "../types";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
})
);
const includeDomains = ["media_player"];
const SCHEMA = [
{
name: "entity",
required: true,
selector: { entity: { domain: "media_player" } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "theme", selector: { theme: {} } },
] as const satisfies readonly HaFormSchema[];
@customElement("hui-media-control-card-editor")
export class HuiMediaControlCardEditor
@@ -34,69 +56,40 @@ export class HuiMediaControlCardEditor
this._config = config;
}
get _entity(): string {
return this._config!.entity || "";
}
get _theme(): string {
return this._config!.theme || "";
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<div class="card-config">
<ha-entity-picker
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)}
.hass=${this.hass}
.value=${this._entity}
.configValue=${"entity"}
.includeDomains=${includeDomains}
.required=${true}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
<ha-theme-picker
.label=${`${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`}
.hass=${this.hass}
.value=${this._theme}
.configValue=${"theme"}
@value-changed=${this._valueChanged}
></ha-theme-picker>
</div>
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: EntitiesEditorEvent): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.target! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
if (target.value === "") {
this._config = { ...this._config };
delete this._config[target.configValue!];
} else {
this._config = {
...this._config,
[target.configValue!]: target.value,
};
}
}
fireEvent(this, "config-changed", { config: this._config });
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
if (schema.name === "theme") {
return `${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.theme"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
};
static styles = configElementStyle;
}
declare global {

View File

@@ -14,6 +14,7 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
@@ -26,6 +27,7 @@ import type { PictureEntityCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign(
@@ -33,7 +35,7 @@ const cardConfigStruct = assign(
object({
entity: optional(string()),
image: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
camera_image: optional(string()),
camera_view: optional(enums(["auto", "live"])),
aspect_ratio: optional(string()),
@@ -65,7 +67,15 @@ export class HuiPictureEntityCardEditor
(localize: LocalizeFunc) =>
[
{ name: "entity", required: true, selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "image", selector: { image: {} } },
{ name: "camera_image", selector: { entity: { domain: "camera" } } },
{

View File

@@ -2,25 +2,35 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { PlantStatusCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
})
);
const SCHEMA = [
{ name: "entity", required: true, selector: { entity: { domain: "plant" } } },
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "theme", selector: { theme: {} } },
] as const;

View File

@@ -12,6 +12,7 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
@@ -20,6 +21,7 @@ import type { HomeAssistant } from "../../../../types";
import type { SensorCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
@@ -27,7 +29,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
icon: optional(string()),
graph: optional(union([literal("line"), literal("none")])),
unit: optional(string()),
@@ -66,7 +68,15 @@ export class HuiSensorCardEditor
entity: { domain: ["counter", "input_number", "number", "sensor"] },
},
},
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",

View File

@@ -14,6 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -29,6 +30,7 @@ import type {
import type { ThermostatCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@@ -50,7 +52,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current_as_primary: optional(boolean()),
features: optional(array(any())),
@@ -84,13 +86,19 @@ export class HuiThermostatCardEditor
name: "entity",
selector: { entity: { domain: ["climate", "water_heater"] } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
schema: [{ name: "theme", selector: { theme: {} } }],
},
...(domain === "climate"
? [

View File

@@ -16,6 +16,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { orderProperties } from "../../../../common/util/order-properties";
import "../../../../components/ha-expansion-panel";
@@ -30,10 +31,7 @@ import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import {
DEFAULT_NAME,
getEntityDefaultTileIconAction,
} from "../../cards/hui-tile-card";
import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card";
import type { TileCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
@@ -105,7 +103,7 @@ export class HuiTileCardEditor
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_NAME,
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },

View File

@@ -12,6 +12,7 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -24,12 +25,13 @@ import type { WeatherForecastCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current: optional(boolean()),
show_forecast: optional(boolean()),
@@ -148,7 +150,15 @@ export class HuiWeatherForecastCardEditor
required: true,
selector: { entity: { domain: "weather" } },
},
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
name: "",
type: "grid",

View File

@@ -153,14 +153,20 @@ export class HuiGraphHeaderFooter
// Message came in before we had a chance to unload
return;
}
this._coordinates =
coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity],
this._config.hours_to_show!,
500,
this._config.detail!,
this._config.limits
) || [];
const width = this.clientWidth || this.offsetWidth;
// sample to 1 point per hour or 1 point per 5 pixels
const maxDetails =
this._config.detail! > 1
? Math.max(width / 5, this._config.hours_to_show!)
: this._config.hours_to_show!;
const { points } = coordinatesMinimalResponseCompressedState(
combinedHistory[this._config.entity],
width,
width / 5,
maxDetails,
{ minY: this._config.limits?.min, maxY: this._config.limits?.max }
);
this._coordinates = points;
},
this._config.hours_to_show!,
[this._config.entity]

View File

@@ -16,7 +16,6 @@ import type {
AreaCardConfig,
HomeSummaryCard,
MarkdownCardConfig,
TileCardConfig,
WeatherForecastCardConfig,
} from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
@@ -131,31 +130,14 @@ export class HomeMainViewStrategy extends ReactiveElement {
const favoriteEntities = (config.favorite_entities || []).filter(
(entityId) => hass.states[entityId] !== undefined
);
if (favoriteEntities.length > 0) {
favoriteSection.cards!.push(
{
type: "heading",
heading: "",
heading_style: "subtitle",
},
...favoriteEntities.map(
(entityId) =>
({
type: "tile",
entity: entityId,
show_entity_picture: true,
}) as TileCardConfig
)
);
}
const maxCommonControls = Math.max(8, favoriteEntities.length);
const commonControlsSection = {
strategy: {
type: "common-controls",
title: hass.localize("ui.panel.lovelace.strategy.home.common_controls"),
limit: 4,
exclude_entities: favoriteEntities,
limit: maxCommonControls,
include_entities: favoriteEntities,
hide_empty: true,
} satisfies CommonControlSectionStrategyConfig,
column_span: maxColumns,

View File

@@ -14,6 +14,7 @@ export interface CommonControlSectionStrategyConfig {
icon?: string;
limit?: number;
exclude_entities?: string[];
include_entities?: string[];
hide_empty?: boolean;
}
@@ -52,12 +53,23 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
(entity) => entity in hass.states
);
if (config.exclude_entities) {
if (config.exclude_entities?.length) {
predictedEntities = predictedEntities.filter(
(entity) => !config.exclude_entities!.includes(entity)
);
}
if (config.include_entities?.length) {
// Remove included entities from predicted list to avoid duplicates
predictedEntities = predictedEntities.filter(
(entity) => !config.include_entities!.includes(entity)
);
// Add included entities to the start of the list
predictedEntities.unshift(
...config.include_entities!.filter((entity) => entity in hass.states)
);
}
const limit = config.limit ?? DEFAULT_LIMIT;
predictedEntities = predictedEntities.slice(0, limit);

View File

@@ -20,6 +20,11 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement {
public closeDialog() {
this._params = undefined;
const img = this.renderRoot.querySelector("img");
if (img) {
// Unload streaming images so the connection can be closed
img.src = "";
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}

View File

@@ -3042,8 +3042,6 @@
"paragraph": "Grid consumption is the energy that flows from the energy grid to your home.",
"entity_para": "Pick a sensor which measures grid consumption in either of {unit}.",
"energy_stat": "Consumed energy",
"power_stat": "Current power consumption",
"power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate consumption, negative values indicate production.",
"cost_para": "Select how Home Assistant should keep track of the costs of the consumed energy.",
"no_cost": "Do not track costs",
"cost_stat": "Use an entity tracking the total costs",
@@ -3058,7 +3056,6 @@
"paragraph": "Grid production is the energy that flows from your solar panels to the grid.",
"entity_para": "Pick a sensor which measures grid production in either of {unit}.",
"energy_stat": "Energy returned to the grid",
"power_stat": "Current power returned to the grid",
"cost_para": "Do you get money back when you return energy to the grid?",
"no_cost": "I do not get money back",
"cost_stat": "Use an entity tracking the total received money",
@@ -3085,7 +3082,6 @@
"header": "Configure solar panels",
"entity_para": "Pick a sensor which measures solar energy production in either of {unit}.",
"solar_production_energy": "Solar production energy",
"solar_production_power": "Solar production power",
"solar_production_forecast": "Solar production forecast",
"solar_production_forecast_description": "Adding solar production forecast information will allow you to quickly see your expected production for today.",
"dont_forecast_production": "Don't forecast production",
@@ -3103,12 +3099,9 @@
"add_battery_system": "Add battery system",
"dialog": {
"header": "Configure battery system",
"energy_helper_into": "Pick a sensor that measures the electricity flowing into the battery in either of {unit}.",
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
"energy_into_battery": "Energy charged into the battery",
"energy_out_of_battery": "Energy discharged from the battery",
"power": "Battery power",
"power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery."
"entity_para": "Pick sensors which measure energy going into and coming out of the battery in either of {unit}.",
"energy_into_battery": "Energy going into the battery",
"energy_out_of_battery": "Energy coming out of the battery"
}
},
"gas": {
@@ -3170,8 +3163,7 @@
"header": "Add a device",
"display_name": "Display name",
"device_consumption_energy": "Device energy consumption",
"device_consumption_power": "Device power consumption",
"selected_stat_intro": "Select the sensor that measures the device's electricity usage in either of {unit}.",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
@@ -9528,8 +9520,8 @@
"description": "This will restart the add-on if it crashes"
},
"auto_update": {
"title": "Autoupdate",
"description": "Autoupdate the add-on when there is a new version available"
"title": "Automatically update",
"description": "Automatically update the add-on when a new version is available"
},
"ingress_panel": {
"title": "Add to sidebar",

View File

@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_ENTITY_NAME } from "../../../../../src/common/entity/compute_entity_name_display";
import { computeLovelaceEntityName } from "../../../../../src/panels/lovelace/common/entity/compute-lovelace-entity-name";
import type { HomeAssistant } from "../../../../../src/types";
import { mockStateObj } from "../../../../common/entity/context/context-mock";
const createMockHass = (
mockFormatEntityName: ReturnType<typeof vi.fn>
): HomeAssistant =>
({
formatEntityName: mockFormatEntityName,
}) as unknown as HomeAssistant;
describe("computeLovelaceEntityName", () => {
it("returns the string directly when nameConfig is a string", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeLovelaceEntityName(hass, stateObj, "Custom Name");
expect(result).toBe("Custom Name");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when nameConfig is empty string", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeLovelaceEntityName(hass, stateObj, "");
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("calls formatEntityName with DEFAULT_ENTITY_NAME when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeLovelaceEntityName(hass, stateObj, undefined);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(
stateObj,
DEFAULT_ENTITY_NAME
);
});
it("calls formatEntityName with EntityNameItem config", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.bedroom" });
const nameConfig = { type: "device" as const };
const result = computeLovelaceEntityName(hass, stateObj, nameConfig);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
it("calls formatEntityName with array of EntityNameItems", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const nameConfig = [
{ type: "device" as const },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, stateObj, nameConfig);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
});