Compare commits

..

17 Commits

Author SHA1 Message Date
Paul Bottein
4abefd5080 Fix custom option selection 2025-10-14 18:48:23 +02:00
Paul Bottein
bd7672b3d0 Make custom text more discoverable in entity name picker 2025-10-14 14:05:21 +02:00
Petar Petrov
fd7f0d3841 Add pie chart mode to energy devices graph (#27282)
* Add pie chart mode to energy devices graph

* universal transition

* format

* Add hide_compound_stats option to energy-devices-graph-card (#27263)

* Add hide_compound_stats option to energy-devices-graph-card

* Update src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* format

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Save chart type in storage

* show untracked compound energy and total energy

* Update dependency lint-staged to v16.2.3 (#27285)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Update dependency @codemirror/view to v6.38.4 (#27288)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Add a sub-editor to hui-entity-editor (#27157)

* Add a sub-editor to hui-entity-editor

* item styling

* fix compare order

* handle label click in pie chart

* order compare data based on current data

* show untracked energy in tooltip

* Apply suggestions from code review

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: karwosts <32912880+karwosts@users.noreply.github.com>
2025-10-14 11:31:11 +00:00
hanwg
36aa74e4a5 Add menu item to copy config entry id (#27394) 2025-10-14 10:26:42 +00:00
Paul Bottein
938128d1c3 Don't add audio track if webrtc player is muted (#25767) 2025-10-14 10:21:59 +00:00
Paul Bottein
2a5d4ac578 Rename security panel to safety panel (#27502) 2025-10-14 12:09:01 +02:00
Jan-Philipp Benecke
be63ff7702 Improve ZHA config dashboard styling (#27492)
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2025-10-14 11:56:19 +02:00
renovate[bot]
132c68bf20 Update dependency lint-staged to v16.2.4 (#27499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 11:04:28 +03:00
renovate[bot]
16499bbd6b Update dependency @rsdoctor/rspack-plugin to v1.3.2 (#27498)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 10:49:45 +03:00
renovate[bot]
c7eddfed8f Update dependency @types/leaflet to v1.9.21 (#27497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-14 10:19:30 +03:00
Jan-Philipp Benecke
150842e431 Fix button wrapping in Z-Wave JS config dashboard (#27493) 2025-10-14 06:10:20 +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
61 changed files with 1405 additions and 886 deletions

View File

@@ -46,22 +46,9 @@ class HassioIngressView extends LitElement {
private _fetchDataTimeout?: number;
private _messageListener = (ev: MessageEvent) => {
if (this._addon?.webui_ha_aware && ev.data?.type === "toggle-sidebar") {
this._toggleMenu();
}
};
public connectedCallback() {
super.connectedCallback();
window.addEventListener("message", this._messageListener);
}
public disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("message", this._messageListener);
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
@@ -96,25 +83,17 @@ class HassioIngressView extends LitElement {
</hass-subpage>`;
}
// If webui_ha_aware is true, or if narrow or sidebar is always hidden,
// don't render the header and just render the iframe
if (
this._addon.webui_ha_aware ||
this.narrow ||
this.hass.dockedSidebar === "always_hidden"
) {
return iframe;
}
return html`<div class="header">
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`;
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header">
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`
: iframe}`;
}
protected async firstUpdated(): Promise<void> {

View File

@@ -157,7 +157,7 @@
"@octokit/auth-oauth-device": "8.0.2",
"@octokit/plugin-retry": "8.0.2",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.1",
"@rsdoctor/rspack-plugin": "1.3.2",
"@rspack/core": "1.5.8",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -167,7 +167,7 @@
"@types/culori": "4.0.1",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.20",
"@types/leaflet": "1.9.21",
"@types/leaflet-draw": "1.0.13",
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
@@ -203,7 +203,7 @@
"husky": "9.1.7",
"jsdom": "27.0.0",
"jszip": "3.10.1",
"lint-staged": "16.2.3",
"lint-staged": "16.2.4",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",

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

@@ -25,6 +25,7 @@ import "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
field_label: string;
value: string;
}
@@ -41,6 +42,23 @@ const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
const formatOptionValue = (item: EntityNameItem) => {
if (item.type === "text" && item.text) {
return item.text;
}
return `___${item.type}___`;
};
const parseOptionValue = (value: string): EntityNameItem => {
if (value.startsWith("___") && value.endsWith("___")) {
const type = value.slice(3, -3);
if (KNOWN_TYPES.has(type)) {
return { type: type as EntityNameType };
}
}
return { type: "text", text: value };
};
@customElement("ha-entity-name-picker")
export class HaEntityNamePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -121,13 +139,23 @@ export class HaEntityNamePicker extends LitElement {
return {
primary,
secondary,
value: name,
field_label: primary,
value: formatOptionValue({ type: name }),
};
});
return items;
});
private _customTextOption = memoizeOne((text: string) => ({
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.use_custom_name"
),
secondary: `"${text}"`,
field_label: text,
value: formatOptionValue({ type: "text", text }),
}));
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
return `"${item.text}"`;
@@ -214,7 +242,7 @@ export class HaEntityNamePicker extends LitElement {
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
item-label-path="field_label"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@@ -286,14 +314,13 @@ export class HaEntityNamePicker extends LitElement {
const initialItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const initialValue = initialItem
? initialItem.type === "text"
? initialItem.text
: initialItem.type
: "";
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem && initialItem.type === "text" && initialItem.text) {
filteredItems.push(this._customTextOption(initialItem.text));
}
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
@@ -326,11 +353,7 @@ export class HaEntityNamePicker extends LitElement {
const currentItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const currentValue = currentItem
? currentItem.type === "text"
? currentItem.text
: currentItem.type
: "";
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
@@ -352,6 +375,7 @@ export class HaEntityNamePicker extends LitElement {
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customTextOption(input));
this._comboBox.filteredItems = filteredItems;
}
@@ -385,9 +409,7 @@ export class HaEntityNamePicker extends LitElement {
return;
}
const item: EntityNameItem = KNOWN_TYPES.has(value as any)
? { type: value as EntityNameType }
: { type: "text", text: value };
const item: EntityNameItem = parseOptionValue(value);
const newValue = [...this._value];

View File

@@ -86,7 +86,8 @@ export class HaCameraStream extends LitElement {
const streams = this._streams(
this._capabilities?.frontend_stream_types,
this._hlsStreams,
this._webRtcStreams
this._webRtcStreams,
this.muted
);
return html`${repeat(
streams,
@@ -190,7 +191,8 @@ export class HaCameraStream extends LitElement {
(
supportedTypes?: StreamType[],
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean },
muted?: boolean
): Stream[] => {
if (__DEMO__) {
return [{ type: MJPEG_STREAM, visible: true }];
@@ -220,9 +222,10 @@ export class HaCameraStream extends LitElement {
if (
hlsStreams.hasVideo &&
hlsStreams.hasAudio &&
!webRtcStreams.hasAudio
!webRtcStreams.hasAudio &&
!muted
) {
// webRTC stream is missing audio, use HLS
// webRTC stream is missing audio and audio is not muted, use HLS
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
if (webRtcStreams.hasVideo) {

View File

@@ -321,6 +321,10 @@ class HaWebRtcPlayer extends LitElement {
if (!this._remoteStream) {
return;
}
// If the track is audio and the player is muted, we do not add it to the stream.
if (event.track.kind === "audio" && this.muted) {
return;
}
this._remoteStream.addTrack(event.track);
if (!this.hasUpdated) {
await this.updateComplete;

View File

@@ -112,7 +112,6 @@ export interface HassioAddonDetails extends HassioAddonInfo {
translations: Record<string, AddonTranslations>;
watchdog: null | boolean;
webui: null | string;
webui_ha_aware?: boolean;
}
export interface HassioAddonsInfo {

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

@@ -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

@@ -33,7 +33,7 @@ const COMPONENTS = {
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
safety: () => import("../panels/safety/ha-panel-safety"),
climate: () => import("../panels/climate/ha-panel-climate"),
};

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

@@ -2,6 +2,7 @@ import {
mdiAlertCircle,
mdiChevronDown,
mdiCogOutline,
mdiContentCopy,
mdiDelete,
mdiDevices,
mdiDotsVertical,
@@ -71,6 +72,8 @@ import {
import "./ha-config-entry-device-row";
import { renderConfigEntryError } from "./ha-config-integration-page";
import "./ha-config-sub-entry-row";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { showToast } from "../../../util/toast";
@customElement("ha-config-entry-row")
class HaConfigEntryRow extends LitElement {
@@ -315,6 +318,13 @@ class HaConfigEntryRow extends LitElement {
)}
</ha-md-menu-item>
<ha-md-menu-item @click=${this._handleCopy} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.copy"
)}
</ha-md-menu-item>
${Object.keys(item.supported_subentry_types).map(
(flowType) =>
html`<ha-md-menu-item
@@ -623,6 +633,15 @@ class HaConfigEntryRow extends LitElement {
});
}
private async _handleCopy() {
await copyToClipboard(this.entry.entry_id);
showToast(this, {
message:
this.hass?.localize("ui.common.copied_clipboard") ||
"Copied to clipboard",
});
}
private async _handleRename() {
const newName = await showPromptDialog(this, {
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),

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: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
`,
];
}

View File

@@ -999,6 +999,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
display: flex;
gap: var(--ha-space-2);
margin-left: auto;
flex-wrap: wrap;
}
.container {

View File

@@ -318,13 +318,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
});
}
if (this.hass.panels.security) {
if (this.hass.panels.safety) {
result.push({
icon: "mdi:security",
title: this.hass.localize("panel.security"),
title: this.hass.localize("panel.safety"),
show_in_sidebar: false,
mode: "storage",
url_path: "security",
url_path: "safety",
filename: "",
iconColor: "var(--blue-grey-color)",
default: false,

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

@@ -2,16 +2,22 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiChartDonut, mdiChartBar } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import type { BarSeriesOption, PieSeriesOption } from "echarts/charts";
import { PieChart } from "echarts/charts";
import type { ECElementEvent } from "echarts/types/dist/shared";
import { filterXSS } from "../../../../common/util/xss";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import type { EnergyData } from "../../../../data/energy";
import { getEnergyDataCollection } from "../../../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getSummedData,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
getStatisticLabel,
@@ -26,6 +32,8 @@ import type { ECOption } from "../../../../resources/echarts";
import "../../../../components/ha-card";
import { fireEvent } from "../../../../common/dom/fire_event";
import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -36,10 +44,20 @@ export class HuiEnergyDevicesGraphCard
@state() private _config?: EnergyDevicesGraphCardConfig;
@state() private _chartData: BarSeriesOption[] = [];
@state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = [];
@state() private _data?: EnergyData;
@state()
@storage({
key: "energy-devices-graph-chart-type",
state: true,
subscribe: false,
})
private _chartType: "bar" | "pie" = "bar";
private _compoundStats: string[] = [];
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@@ -76,9 +94,16 @@ export class HuiEnergyDevicesGraphCard
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div class="card-header">
<span>${this._config.title ? this._config.title : nothing}</span>
<ha-icon-button
.path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>
</div>
<div
class="content ${classMap({
"has-header": !!this._config.title,
@@ -87,9 +112,10 @@ export class HuiEnergyDevicesGraphCard
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(this._chartData)}
.height=${`${(this._chartData[0]?.data?.length || 0) * 28 + 50}px`}
.options=${this._createOptions(this._chartData, this._chartType)}
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
@chart-click=${this._handleChartClick}
.extraComponents=${[PieChart]}
></ha-chart-base>
</div>
</ha-card>
@@ -97,71 +123,86 @@ export class HuiEnergyDevicesGraphCard
}
private _renderTooltip(params: any) {
const deviceName = filterXSS(this._getDeviceName(params.value[1]));
const deviceName = filterXSS(this._getDeviceName(params.name));
const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`;
const value = `${formatNumber(
params.value[0] as number,
this.hass.locale,
params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh`;
return `${title}${params.marker} ${params.seriesName}: ${value}`;
}
private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return {
xAxis: {
type: "value",
name: "kWh",
},
yAxis: {
type: "category",
inverse: true,
triggerEvent: true,
// take order from data
data: data[0]?.data?.map((d: any) => d.value[1]),
axisLabel: {
formatter: this._getDeviceName.bind(this),
overflow: "truncate",
fontSize: 12,
margin: 5,
width: Math.min(
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5
) || [])
)
),
private _createOptions = memoizeOne(
(
data: (BarSeriesOption | PieSeriesOption)[],
chartType: "bar" | "pie"
): ECOption => {
const options: ECOption = {
grid: {
top: 5,
left: 5,
right: 40,
bottom: 0,
containLabel: true,
},
},
grid: {
top: 5,
left: 5,
right: 40,
bottom: 0,
containLabel: true,
},
tooltip: {
show: true,
formatter: this._renderTooltip.bind(this),
},
};
});
tooltip: {
show: true,
formatter: this._renderTooltip.bind(this),
},
xAxis: { show: false },
yAxis: { show: false },
};
if (chartType === "bar") {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
options.xAxis = {
show: true,
type: "value",
name: "kWh",
};
options.yAxis = {
show: true,
type: "category",
inverse: true,
triggerEvent: true,
// take order from data
data: data[0]?.data?.map((d: any) => d.name),
axisLabel: {
formatter: this._getDeviceName.bind(this),
overflow: "truncate",
fontSize: 12,
margin: 5,
width: Math.min(
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
measureTextWidth(this._getDeviceName(d.name), 12) + 5
) || [])
)
),
},
};
}
return options;
}
);
private _getDeviceName(statisticId: string): string {
const suffix = this._compoundStats.includes(statisticId)
? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})`
: "";
return (
this._data?.prefs.device_consumption.find(
(this._data?.prefs.device_consumption.find(
(d) => d.stat_consumption === statisticId
)?.name ||
getStatisticLabel(
this.hass,
statisticId,
this._data?.statsMetadata[statisticId]
)
getStatisticLabel(
this.hass,
statisticId,
this._data?.statsMetadata[statisticId]
)) + suffix
);
}
@@ -169,60 +210,105 @@ export class HuiEnergyDevicesGraphCard
const data = energyData.stats;
const compareData = energyData.statsCompare;
const chartData: NonNullable<BarSeriesOption["data"]> = [];
const chartDataCompare: NonNullable<BarSeriesOption["data"]> = [];
const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> =
[];
const chartDataCompare: NonNullable<
(BarSeriesOption | PieSeriesOption)["data"]
> = [];
const datasets: BarSeriesOption[] = [
const datasets: (BarSeriesOption | PieSeriesOption)[] = [
{
type: "bar",
type: this._chartType,
radius: [compareData ? "50%" : "40%", "70%"],
universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage"
),
itemStyle: {
borderRadius: [0, 4, 4, 0],
borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartData,
barWidth: compareData ? 10 : 20,
cursor: "default",
},
minShowLabelAngle: 15,
label:
this._chartType === "pie"
? {
formatter: ({ name }) => this._getDeviceName(name),
}
: undefined,
} as BarSeriesOption | PieSeriesOption,
];
if (compareData) {
datasets.push({
type: "bar",
type: this._chartType,
radius: ["30%", "50%"],
universalTransition: true,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage"
),
itemStyle: {
borderRadius: [0, 4, 4, 0],
borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4,
},
data: chartDataCompare,
barWidth: 10,
cursor: "default",
});
label: this._chartType === "pie" ? { show: false } : undefined,
emphasis:
this._chartType === "pie"
? {
focus: "series",
blurScope: "global",
}
: undefined,
} as BarSeriesOption | PieSeriesOption);
}
const computedStyle = getComputedStyle(this);
const exclude = this._config?.hide_compound_stats
? energyData.prefs.device_consumption
.map((d) => d.included_in_stat)
.filter(Boolean)
: [];
this._compoundStats = energyData.prefs.device_consumption
.map((d) => d.included_in_stat)
.filter(Boolean) as string[];
energyData.prefs.device_consumption.forEach((device, id) => {
if (exclude.includes(device.stat_consumption)) {
return;
}
const value =
const devices = energyData.prefs.device_consumption;
const devicesTotals: Record<string, number> = {};
devices.forEach((device) => {
devicesTotals[device.stat_consumption] =
device.stat_consumption in data
? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0
: 0;
const color = getGraphColorByIndex(id, computedStyle);
});
const devicesTotalsCompare: Record<string, number> = {};
if (compareData) {
devices.forEach((device) => {
devicesTotalsCompare[device.stat_consumption] =
device.stat_consumption in compareData
? calculateStatisticSumGrowth(
compareData[device.stat_consumption]
) || 0
: 0;
});
}
devices.forEach((device, idx) => {
let value = devicesTotals[device.stat_consumption];
if (!this._config?.hide_compound_stats) {
const childSum = devices.reduce((acc, d) => {
if (d.included_in_stat === device.stat_consumption) {
return acc + devicesTotals[d.stat_consumption];
}
return acc;
}, 0);
value -= Math.min(value, childSum);
} else if (this._compoundStats.includes(device.stat_consumption)) {
return;
}
const color = getGraphColorByIndex(idx, computedStyle);
chartData.push({
id,
value: [value, device.stat_consumption],
id: device.stat_consumption,
value: [value, device.stat_consumption] as any,
name: device.stat_consumption,
itemStyle: {
color: color + "7F",
borderColor: color,
@@ -230,16 +316,24 @@ export class HuiEnergyDevicesGraphCard
});
if (compareData) {
const compareValue =
let compareValue =
device.stat_consumption in compareData
? calculateStatisticSumGrowth(
compareData[device.stat_consumption]
) || 0
: 0;
const compareChildSum = devices.reduce((acc, d) => {
if (d.included_in_stat === device.stat_consumption) {
return acc + devicesTotalsCompare[d.stat_consumption];
}
return acc;
}, 0);
compareValue -= Math.min(compareValue, compareChildSum);
chartDataCompare.push({
id,
value: [compareValue, device.stat_consumption],
id: device.stat_consumption,
value: [compareValue, device.stat_consumption] as any,
name: device.stat_consumption,
itemStyle: {
color: color + "32",
borderColor: color + "7F",
@@ -249,11 +343,62 @@ export class HuiEnergyDevicesGraphCard
});
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
chartData.length = Math.min(
this._config?.max_devices || Infinity,
chartData.length
);
datasets.forEach((dataset) => {
dataset.data!.length = Math.min(
this._config?.max_devices || Infinity,
dataset.data!.length
);
});
if (this._chartType === "pie") {
const { summedData } = getSummedData(energyData);
const { consumption } = computeConsumptionData(summedData);
const totalUsed = consumption.total.used_total;
const showUntracked =
"from_grid" in summedData ||
"solar" in summedData ||
"from_battery" in summedData;
const untracked = showUntracked
? totalUsed -
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
: 0;
datasets.push({
type: "pie",
radius: ["0%", compareData ? "30%" : "40%"],
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage"
),
data: [totalUsed],
label: {
show: true,
position: "center",
color: computedStyle.getPropertyValue("--secondary-text-color"),
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
lineHeight: 24,
fontWeight: "bold",
formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`,
},
cursor: "default",
itemStyle: {
color: "rgba(0, 0, 0, 0)",
},
tooltip: {
formatter: () =>
untracked > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked",
{ num: formatNumber(untracked, this.hass.locale) }
)
: "",
},
});
}
this._chartData = datasets;
await this.updateComplete;
@@ -268,11 +413,26 @@ export class HuiEnergyDevicesGraphCard
fireEvent(this, "hass-more-info", {
entityId: e.detail.value as string,
});
} else if (
e.detail.seriesType === "pie" &&
e.detail.event?.target?.type === "tspan" // label
) {
fireEvent(this, "hass-more-info", {
entityId: (e.detail.data as any).id as string,
});
}
}
private _handleChartTypeChange(): void {
this._chartType = this._chartType === "pie" ? "bar" : "pie";
this._getStatistics(this._data!);
}
static styles = css`
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 0;
}
.content {
@@ -284,6 +444,11 @@ export class HuiEnergyDevicesGraphCard
ha-chart-base {
--chart-max-height: none;
}
ha-icon-button {
transform: rotate(90deg);
color: var(--secondary-text-color);
cursor: pointer;
}
`;
}

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

@@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types";
const COLORS: Record<HomeSummary, string> = {
light: "amber",
climate: "deep-orange",
security: "blue-grey",
safety: "blue-grey",
media_players: "blue",
};
@@ -147,23 +147,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
? `${formattedMinTemp}°`
: `${formattedMinTemp} - ${formattedMaxTemp}°`;
}
case "security": {
case "safety": {
// Alarm and lock status
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) =>
generateEntityFilter(this.hass!, filter)
);
const securityEntities = findEntities(
entitiesInsideArea,
securityFilters
);
const safetyEntities = findEntities(entitiesInsideArea, safetyFilters);
const locks = securityEntities.filter((entityId) => {
const locks = safetyEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "lock";
});
const alarms = securityEntities.filter((entityId) => {
const alarms = safetyEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "alarm_control_panel";
});

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

@@ -48,7 +48,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
light: () => import("../../light/strategies/light-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
safety: () => import("../../safety/strategies/safety-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),
},
section: {

View File

@@ -2,12 +2,12 @@ import type { EntityFilter } from "../../../../../common/entity/entity_filter";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
import { lightEntityFilters } from "../../../../light/strategies/light-view-strategy";
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
import { safetyEntityFilters } from "../../../../safety/strategies/safety-view-strategy";
export const HOME_SUMMARIES = [
"light",
"climate",
"security",
"safety",
"media_players",
] as const;
@@ -16,14 +16,14 @@ export type HomeSummary = (typeof HOME_SUMMARIES)[number];
export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
light: "mdi:lamps",
climate: "mdi:home-thermometer",
security: "mdi:security",
safety: "mdi:security",
media_players: "mdi:multimedia",
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
light: lightEntityFilters,
climate: climateEntityFilters,
security: securityEntityFilters,
safety: safetyEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }],
};
@@ -31,7 +31,7 @@ export const getSummaryLabel = (
localize: LocalizeFunc,
summary: HomeSummary
) => {
if (summary === "light" || summary === "climate" || summary === "security") {
if (summary === "light" || summary === "climate" || summary === "safety") {
return localize(`panel.${summary}`);
}
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);

View File

@@ -104,7 +104,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
const {
light,
climate,
security,
safety,
media_players: mediaPlayers,
} = entitiesBySummary;
@@ -115,7 +115,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
computeHeadingCard(
getSummaryLabel(hass.localize, "light"),
HOME_SUMMARIES_ICONS.light,
"/lights?historyBack=1"
"/light?historyBack=1"
),
...light.map(computeTileCard),
],
@@ -129,23 +129,23 @@ export class HomeAreaViewStrategy extends ReactiveElement {
computeHeadingCard(
getSummaryLabel(hass.localize, "climate"),
HOME_SUMMARIES_ICONS.climate,
"climate"
"/climate?historyBack=1"
),
...climate.map(computeTileCard),
],
});
}
if (security.length > 0) {
if (safety.length > 0) {
sections.push({
type: "grid",
cards: [
computeHeadingCard(
getSummaryLabel(hass.localize, "security"),
HOME_SUMMARIES_ICONS.security,
"security"
getSummaryLabel(hass.localize, "safety"),
HOME_SUMMARIES_ICONS.safety,
"/safety?historyBack=1"
),
...security.map(computeTileCard),
...safety.map(computeTileCard),
],
});
}
@@ -157,7 +157,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
computeHeadingCard(
getSummaryLabel(hass.localize, "media_players"),
HOME_SUMMARIES_ICONS.media_players,
"media-players"
"/media-players"
),
...mediaPlayers.map(computeTileCard),
],

View File

@@ -179,11 +179,11 @@ export class HomeMainViewStrategy extends ReactiveElement {
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "security",
summary: "safety",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
navigation_path: "/safety?historyBack=1",
},
grid_options: {
rows: 2,

View File

@@ -11,18 +11,18 @@ import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const SECURITY_LOVELACE_CONFIG: LovelaceConfig = {
const SAFETY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "security",
type: "safety",
},
},
],
};
@customElement("ha-panel-security")
class PanelSecurity extends LitElement {
@customElement("ha-panel-safety")
class PanelSafety extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -69,7 +69,7 @@ class PanelSecurity extends LitElement {
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.security")}</div>
<div class="main-title">${this.hass.localize("panel.safety")}</div>
</div>
</div>
@@ -86,10 +86,10 @@ class PanelSecurity extends LitElement {
private _setLovelace() {
this._lovelace = {
config: SECURITY_LOVELACE_CONFIG,
rawConfig: SECURITY_LOVELACE_CONFIG,
config: SAFETY_LOVELACE_CONFIG,
rawConfig: SAFETY_LOVELACE_CONFIG,
editMode: false,
urlPath: "security",
urlPath: "safety",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
@@ -191,6 +191,6 @@ class PanelSecurity extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-panel-security": PanelSecurity;
"ha-panel-safety": PanelSafety;
}
}

View File

@@ -16,11 +16,11 @@ import {
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface SecurityViewStrategyConfig {
type: "security";
export interface SafetyViewStrategyConfig {
type: "safety";
}
export const securityEntityFilters: EntityFilter[] = [
export const safetyEntityFilters: EntityFilter[] = [
{
domain: "camera",
entity_category: "none",
@@ -66,7 +66,7 @@ export const securityEntityFilters: EntityFilter[] = [
},
];
const processAreasForSecurity = (
const processAreasForSafety = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
@@ -80,12 +80,12 @@ const processAreasForSecurity = (
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaSecurityEntities = entities.filter(areaFilter);
const areaSafetyEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of areaSecurityEntities) {
for (const entityId of areaSafetyEntities) {
areaCards.push(computeTileCard(entityId));
}
@@ -102,10 +102,10 @@ const processAreasForSecurity = (
return cards;
};
@customElement("security-view-strategy")
export class SecurityViewStrategy extends ReactiveElement {
@customElement("safety-view-strategy")
export class SafetyViewStrategy extends ReactiveElement {
static async generate(
_config: SecurityViewStrategyConfig,
_config: SafetyViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
@@ -116,11 +116,11 @@ export class SecurityViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const securityFilters = securityEntityFilters.map((filter) =>
const safetyFilters = safetyEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, securityFilters);
const entities = findEntities(allEntities, safetyFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
@@ -144,7 +144,7 @@ export class SecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(areaIds, hass, entities);
const areaCards = processAreasForSafety(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -168,7 +168,7 @@ export class SecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(home.areas, hass, entities);
const areaCards = processAreasForSafety(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -186,6 +186,6 @@ export class SecurityViewStrategy extends ReactiveElement {
declare global {
interface HTMLElementTagNameMap {
"security-view-strategy": SecurityViewStrategy;
"safety-view-strategy": SafetyViewStrategy;
}
}

View File

@@ -12,7 +12,7 @@
"media_browser": "Media",
"profile": "Profile",
"light": "Lights",
"security": "Security",
"safety": "Safety",
"climate": "Climate"
},
"state": {
@@ -666,7 +666,8 @@
"floor_missing": "No floor assigned",
"device_missing": "No related device"
},
"add": "Add"
"add": "Add",
"use_custom_name": "Use custom name"
},
"entity-attribute-picker": {
"attribute": "Attribute",
@@ -5525,6 +5526,7 @@
"entries": "{count} {count, plural,\n one {entry}\n other {entries}\n}",
"no_devices_or_entities": "No devices or entities",
"devices_without_subentry": "Devices that don't belong to a sub-entry",
"copy": "Copy entry ID",
"rename": "Rename",
"configure": "Configure",
"system_options": "System options",
@@ -7029,7 +7031,11 @@
},
"energy_devices_graph": {
"energy_usage": "Energy usage",
"previous_energy_usage": "Previous energy usage"
"previous_energy_usage": "Previous energy usage",
"total_energy_usage": "Total energy usage",
"change_chart_type": "Change chart type",
"untracked": "untracked",
"includes_untracked": "Includes {num} kWh of untracked energy"
},
"energy_devices_detail_graph": {
"untracked_consumption": "Untracked consumption",
@@ -9520,8 +9526,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);
});
});

248
yarn.lock
View File

@@ -3900,82 +3900,101 @@ __metadata:
languageName: node
linkType: hard
"@rsdoctor/client@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/client@npm:1.3.1"
checksum: 10/6885dd7e16f2172ddf5d4c901275af77640402f1b5cfd41b0eab6e695cad423bda2909a5b13dc68e864e9d9df4440587b8a3403138437c20b6e3bb15d0c83b04
"@rsbuild/plugin-check-syntax@npm:1.4.0":
version: 1.4.0
resolution: "@rsbuild/plugin-check-syntax@npm:1.4.0"
dependencies:
acorn: "npm:^8.15.0"
browserslist-to-es-version: "npm:^1.1.0"
htmlparser2: "npm:10.0.0"
picocolors: "npm:^1.1.1"
source-map: "npm:^0.7.6"
peerDependencies:
"@rsbuild/core": 1.x
peerDependenciesMeta:
"@rsbuild/core":
optional: true
checksum: 10/1e6ee37cc072bbf459ecd493a78cab2798e08c2e046828e0d93f618581738a4b7a6b6f3bf8b890a58b67b5a879d0a778810b33b834f75dadd89817cf16c73a38
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/core@npm:1.3.1"
"@rsdoctor/client@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/client@npm:1.3.2"
checksum: 10/cc6d82453976e3231c141231b474043eb8e55beae2266742993019888934a66b839173374eec5af1374970c31b6e0ac67171031e35ac0c246b2e73b2f0d46c60
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/core@npm:1.3.2"
dependencies:
"@rsdoctor/graph": "npm:1.3.1"
"@rsdoctor/sdk": "npm:1.3.1"
"@rsdoctor/types": "npm:1.3.1"
"@rsdoctor/utils": "npm:1.3.1"
"@rsbuild/plugin-check-syntax": "npm:1.4.0"
"@rsdoctor/graph": "npm:1.3.2"
"@rsdoctor/sdk": "npm:1.3.2"
"@rsdoctor/types": "npm:1.3.2"
"@rsdoctor/utils": "npm:1.3.2"
browserslist-load-config: "npm:^1.0.1"
enhanced-resolve: "npm:5.12.0"
filesize: "npm:^10.1.6"
fs-extra: "npm:^11.1.1"
lodash-es: "npm:^4.17.21"
semver: "npm:^7.7.2"
semver: "npm:^7.7.3"
source-map: "npm:^0.7.6"
checksum: 10/40f4de3680202487ff094cd97664035c19c8bd802ff9adbd4c3947c53b08e738eac65e22b45514ca1cd2640305451c53d1efd23a0097674d4af0391698eff9a7
checksum: 10/56f3fb3b12250bdc4140b50f6681b768475d014e243ca892f35f072153a292f63f014d38f6715d1b58707f93950b5f9109c823c9eb8f33c55475922eda765cc2
languageName: node
linkType: hard
"@rsdoctor/graph@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/graph@npm:1.3.1"
"@rsdoctor/graph@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/graph@npm:1.3.2"
dependencies:
"@rsdoctor/types": "npm:1.3.1"
"@rsdoctor/utils": "npm:1.3.1"
"@rsdoctor/types": "npm:1.3.2"
"@rsdoctor/utils": "npm:1.3.2"
lodash.unionby: "npm:^4.8.0"
path-browserify: "npm:1.0.1"
source-map: "npm:^0.7.6"
checksum: 10/7ae4abd2bd630e2589975df3e34d029921c2ff34c9f62961aff73c384dbb7e94d24faf2bf3f5118860f56b9bab2a5cd4b5185c178ce91f8a0852a258a854602c
checksum: 10/ecdb653e603656bac1715383d968e544349294db4082cf094b138501650ceac24c3037f27c503e7507e7419199f559e3628cc4aa5091c753a48e88960a9ded61
languageName: node
linkType: hard
"@rsdoctor/rspack-plugin@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/rspack-plugin@npm:1.3.1"
"@rsdoctor/rspack-plugin@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/rspack-plugin@npm:1.3.2"
dependencies:
"@rsdoctor/core": "npm:1.3.1"
"@rsdoctor/graph": "npm:1.3.1"
"@rsdoctor/sdk": "npm:1.3.1"
"@rsdoctor/types": "npm:1.3.1"
"@rsdoctor/utils": "npm:1.3.1"
"@rsdoctor/core": "npm:1.3.2"
"@rsdoctor/graph": "npm:1.3.2"
"@rsdoctor/sdk": "npm:1.3.2"
"@rsdoctor/types": "npm:1.3.2"
"@rsdoctor/utils": "npm:1.3.2"
lodash-es: "npm:^4.17.21"
peerDependencies:
"@rspack/core": "*"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/94759bf214102e8acffeaaeb89d8274301f0b420274bf6f26afa736ac915f029e02e33cbc4f9f977d208e20a5e38bf3d812a1147be830dcd25a49755ff111d6d
checksum: 10/b9d1feb6448a3004b34d2c3f77db62dfa4207ba6bf576fb92c3e0ceb55e35795d175662970e3dd2c4b8b324302579e987581e9112dfd7d54f27b5a3f0d29d4c5
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/sdk@npm:1.3.1"
"@rsdoctor/sdk@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/sdk@npm:1.3.2"
dependencies:
"@rsdoctor/client": "npm:1.3.1"
"@rsdoctor/graph": "npm:1.3.1"
"@rsdoctor/types": "npm:1.3.1"
"@rsdoctor/utils": "npm:1.3.1"
"@rsdoctor/client": "npm:1.3.2"
"@rsdoctor/graph": "npm:1.3.2"
"@rsdoctor/types": "npm:1.3.2"
"@rsdoctor/utils": "npm:1.3.2"
safer-buffer: "npm:2.1.2"
socket.io: "npm:4.8.1"
tapable: "npm:2.2.3"
checksum: 10/194efba86d15e86d81de3b1a747c3e82874f69c4e3f1f96e9f36f8a83cabbcc6371729498e2ab82724550f376dd2630849c435841031a0c139406aeb4b472d06
checksum: 10/06149043259b90d5bd5a0e8f19dfebbcf9f8e6b698c4bc67a49b28c939094797e7923455914cbc30512723f9dfe557a7d00cdf4bb07285c6e8f27679cab667b9
languageName: node
linkType: hard
"@rsdoctor/types@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/types@npm:1.3.1"
"@rsdoctor/types@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/types@npm:1.3.2"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -3989,16 +4008,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/e058017b77b4b58c22c39a0f1177e6cabdedbdebc355f936bbc6be3ace51279d0cd078e2cab19543a5fe2d4cff3e9980f076c4d18bd70ab3d393d5ce0dd1eb89
checksum: 10/168e59d0f8fa2cda7451746cc071bcddaadb69ce322c99eb730ab7004fe4dee57d52317f6f510020e65fe88045bab906a93d4732a43c53ef67b1cd2d6f889109
languageName: node
linkType: hard
"@rsdoctor/utils@npm:1.3.1":
version: 1.3.1
resolution: "@rsdoctor/utils@npm:1.3.1"
"@rsdoctor/utils@npm:1.3.2":
version: 1.3.2
resolution: "@rsdoctor/utils@npm:1.3.2"
dependencies:
"@babel/code-frame": "npm:7.26.2"
"@rsdoctor/types": "npm:1.3.1"
"@rsdoctor/types": "npm:1.3.2"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-attributes: "npm:^1.9.5"
@@ -4012,7 +4031,7 @@ __metadata:
picocolors: "npm:^1.1.1"
rslog: "npm:^1.2.11"
strip-ansi: "npm:^6.0.1"
checksum: 10/ebe1a7233179bf9be0272959c16fc2fc89c37c2cc2553973002889ab8432697f2bee6308dc1c82208ddb1d13d875be6341b9a985d9fe18536af381989200dc48
checksum: 10/f1523fd9906c42642e7af4904d7d9c74e1de8158905d54102f2ac939ec6a4f48122f552fa88a8aa7e6bdd19044066808844bb1f98fe0a3772f0dc0f4f2b5753a
languageName: node
linkType: hard
@@ -4694,12 +4713,12 @@ __metadata:
languageName: node
linkType: hard
"@types/leaflet@npm:1.9.20, @types/leaflet@npm:^1.9":
version: 1.9.20
resolution: "@types/leaflet@npm:1.9.20"
"@types/leaflet@npm:1.9.21, @types/leaflet@npm:^1.9":
version: 1.9.21
resolution: "@types/leaflet@npm:1.9.21"
dependencies:
"@types/geojson": "npm:*"
checksum: 10/d0d2d907b47264ff3440f1bd27132892b21c43b6e94fe921568ae2c79b0b5648b61d069e2c302e972df786c0395008a62da140d45602fb30b3e5e6a5441be924
checksum: 10/a02eff00db3cbc374c67fa7c0df6c564918482fa00407146bfea19f92600a4009f39af7bb9e3face39845979c133bb67b5bd972472720beef1f285596f47a6d1
languageName: node
linkType: hard
@@ -6150,12 +6169,12 @@ __metadata:
languageName: node
linkType: hard
"baseline-browser-mapping@npm:^2.8.3":
version: 2.8.6
resolution: "baseline-browser-mapping@npm:2.8.6"
"baseline-browser-mapping@npm:^2.8.9":
version: 2.8.16
resolution: "baseline-browser-mapping@npm:2.8.16"
bin:
baseline-browser-mapping: dist/cli.js
checksum: 10/05c89fb1aa864a2a3b5fc9b7f3a4ed3e102ae4d6fa9ccf96a2b8f57fd0c995fb8b4e9ea3152b34c5661533a198026b713e1be415e96473322705b2fbd8dddc48
checksum: 10/52a5807591daeffc810b783b1afa20c4017dd94e5bb74934bcde4dd408758e492610e330cfe6e609a0f0bde5ce210dd934271540fb931389d6838db17ec8cfef
languageName: node
linkType: hard
@@ -6302,6 +6321,15 @@ __metadata:
languageName: node
linkType: hard
"browserslist-to-es-version@npm:^1.1.0":
version: 1.1.1
resolution: "browserslist-to-es-version@npm:1.1.1"
dependencies:
browserslist: "npm:^4.25.1"
checksum: 10/efce2f27e67bda030ee3957f6df5aaa8594ee5cf017d3567f1226f6abcea7c3840c1ba73105b6bd661c5f57339e44c6819804609e1f064cfdfb8d53894a9f777
languageName: node
linkType: hard
"browserslist-useragent-regexp@npm:4.1.3":
version: 4.1.3
resolution: "browserslist-useragent-regexp@npm:4.1.3"
@@ -6320,18 +6348,18 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:^4.24.0, browserslist@npm:^4.25.3":
version: 4.26.2
resolution: "browserslist@npm:4.26.2"
"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.25.3":
version: 4.26.3
resolution: "browserslist@npm:4.26.3"
dependencies:
baseline-browser-mapping: "npm:^2.8.3"
caniuse-lite: "npm:^1.0.30001741"
electron-to-chromium: "npm:^1.5.218"
baseline-browser-mapping: "npm:^2.8.9"
caniuse-lite: "npm:^1.0.30001746"
electron-to-chromium: "npm:^1.5.227"
node-releases: "npm:^2.0.21"
update-browserslist-db: "npm:^1.1.3"
bin:
browserslist: cli.js
checksum: 10/7f732f1a9c18c510aa146270d704b7b1acab52c9922147d453eecd70c926f21d97c7ac10f5303668d444fa60bd3b8778a63a797be249b0d348af4c3a644fa530
checksum: 10/49add06fd753a2514d84c75a7de8d9fb3d70be675e53b72981d87f0c0ff40d8a8cd0bd92f77400381704be0bf1c9c5c65aef95d03843d69475ff55188aa12124
languageName: node
linkType: hard
@@ -6482,10 +6510,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001741":
version: 1.0.30001743
resolution: "caniuse-lite@npm:1.0.30001743"
checksum: 10/e55b13b4a547c9f610a68d5f5668a3239ada4a4aef5d198860397757ab7c03a5b0590675b7e82c5d3d57316b40499e1da52decb08a97ef3066a338871bbb5c37
"caniuse-lite@npm:^1.0.30001746":
version: 1.0.30001750
resolution: "caniuse-lite@npm:1.0.30001750"
checksum: 10/2b912758d817cd2c2c179246e282f8b598695ec733bc446183e1d381eada60889c4770a1dfd86075e046a43d55f9922e2eaed1501347fcb12a38716cc14be297
languageName: node
linkType: hard
@@ -7349,6 +7377,17 @@ __metadata:
languageName: node
linkType: hard
"dom-serializer@npm:^2.0.0":
version: 2.0.0
resolution: "dom-serializer@npm:2.0.0"
dependencies:
domelementtype: "npm:^2.3.0"
domhandler: "npm:^5.0.2"
entities: "npm:^4.2.0"
checksum: 10/e3bf9027a64450bca0a72297ecdc1e3abb7a2912268a9f3f5d33a2e29c1e2c3502c6e9f860fc6625940bfe0cfb57a44953262b9e94df76872fdfb8151097eeb3
languageName: node
linkType: hard
"dom5@npm:^3.0.1":
version: 3.0.1
resolution: "dom5@npm:3.0.1"
@@ -7360,6 +7399,33 @@ __metadata:
languageName: node
linkType: hard
"domelementtype@npm:^2.3.0":
version: 2.3.0
resolution: "domelementtype@npm:2.3.0"
checksum: 10/ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6
languageName: node
linkType: hard
"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3":
version: 5.0.3
resolution: "domhandler@npm:5.0.3"
dependencies:
domelementtype: "npm:^2.3.0"
checksum: 10/809b805a50a9c6884a29f38aec0a4e1b4537f40e1c861950ed47d10b049febe6b79ab72adaeeebb3cc8fc1cd33f34e97048a72a9265103426d93efafa78d3e96
languageName: node
linkType: hard
"domutils@npm:^3.2.1":
version: 3.2.2
resolution: "domutils@npm:3.2.2"
dependencies:
dom-serializer: "npm:^2.0.0"
domelementtype: "npm:^2.3.0"
domhandler: "npm:^5.0.3"
checksum: 10/2e08842151aa406f50fe5e6d494f4ec73c2373199fa00d1f77b56ec604e566b7f226312ae35ab8160bb7f27a27c7285d574c8044779053e499282ca9198be210
languageName: node
linkType: hard
"dot-case@npm:^3.0.4":
version: 3.0.4
resolution: "dot-case@npm:3.0.4"
@@ -7439,10 +7505,10 @@ __metadata:
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.5.218":
version: 1.5.222
resolution: "electron-to-chromium@npm:1.5.222"
checksum: 10/f1f7b21598ddf77a8e44f8e288bc0fb5c82c110ae1df7174a188ea7d2f81851d8e693d46ad2916e2ae0014b7368d331f8c5bee5175e572d7180f91153b251f8d
"electron-to-chromium@npm:^1.5.227":
version: 1.5.235
resolution: "electron-to-chromium@npm:1.5.235"
checksum: 10/fbc227d58a07dbb1b01e4a0f624a2fae03881f160a7c2e4416a68f30c83c1ca29b8f0e04056cb2851a6f493ebaf0d3b24bc2c7721d9e779cccbc9faeffef1c0e
languageName: node
linkType: hard
@@ -7551,7 +7617,7 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^4.4.0":
"entities@npm:^4.2.0, entities@npm:^4.4.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10/ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48
@@ -9244,7 +9310,7 @@ __metadata:
"@octokit/plugin-retry": "npm:8.0.2"
"@octokit/rest": "npm:22.0.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.3.1"
"@rsdoctor/rspack-plugin": "npm:1.3.2"
"@rspack/core": "npm:1.5.8"
"@rspack/dev-server": "npm:1.1.4"
"@swc/helpers": "npm:0.5.17"
@@ -9258,7 +9324,7 @@ __metadata:
"@types/culori": "npm:4.0.1"
"@types/html-minifier-terser": "npm:7.0.2"
"@types/js-yaml": "npm:4.0.9"
"@types/leaflet": "npm:1.9.20"
"@types/leaflet": "npm:1.9.21"
"@types/leaflet-draw": "npm:1.0.13"
"@types/leaflet.markercluster": "npm:1.5.6"
"@types/lodash.merge": "npm:4.6.9"
@@ -9324,7 +9390,7 @@ __metadata:
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
lint-staged: "npm:16.2.3"
lint-staged: "npm:16.2.4"
lit: "npm:3.3.1"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.1"
@@ -9452,6 +9518,18 @@ __metadata:
languageName: node
linkType: hard
"htmlparser2@npm:10.0.0":
version: 10.0.0
resolution: "htmlparser2@npm:10.0.0"
dependencies:
domelementtype: "npm:^2.3.0"
domhandler: "npm:^5.0.3"
domutils: "npm:^3.2.1"
entities: "npm:^6.0.0"
checksum: 10/768870f0e020dca19dc45df206cb6ac466c5dba6566c8fca4ca880347eed409f9977028d08644ac516bca8628ac9c7ded5a3847dc3ee1c043f049abf9e817154
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.2.0
resolution: "http-cache-semantics@npm:4.2.0"
@@ -10659,20 +10737,20 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:16.2.3":
version: 16.2.3
resolution: "lint-staged@npm:16.2.3"
"lint-staged@npm:16.2.4":
version: 16.2.4
resolution: "lint-staged@npm:16.2.4"
dependencies:
commander: "npm:^14.0.1"
listr2: "npm:^9.0.4"
micromatch: "npm:^4.0.8"
nano-spawn: "npm:^1.0.3"
nano-spawn: "npm:^2.0.0"
pidtree: "npm:^0.6.0"
string-argv: "npm:^0.3.2"
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/7c83cb478aa8004eecc8c91d633abe2865ffc037957ae9ee2669e49b76b76fe3512ba431277efc29cec7a38641e7d8a62f3378a41b624c88bde6fbef5524e2cb
checksum: 10/e4ce8e6b07fc2c1d96962dafaab483271b2359b6f22f74324ba0f827ad6383caa2800651379f36b2570cfa74b27354e0db9316be69795636a0c53fa3fd599b79
languageName: node
linkType: hard
@@ -11307,10 +11385,10 @@ __metadata:
languageName: node
linkType: hard
"nano-spawn@npm:^1.0.3":
version: 1.0.3
resolution: "nano-spawn@npm:1.0.3"
checksum: 10/72c56e68ae733c81c459a338fd51e2aa3be06b1cca746c2abe83df7acfac7eee008b01833f5a8781f4ac9fc1eafd23036a44755257a669dfcc2ff2453850822a
"nano-spawn@npm:^2.0.0":
version: 2.0.0
resolution: "nano-spawn@npm:2.0.0"
checksum: 10/117d35d7bd85b146908de5d3d1177d2b2ee3174e5d884d6bc9555583bf6e50a265f4038b5c134b7cdd768a10d53598ccde5c00d6f55e25e7eed31b86b8d29646
languageName: node
linkType: hard
@@ -12925,12 +13003,12 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.7.2":
version: 7.7.2
resolution: "semver@npm:7.7.2"
"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.7.3":
version: 7.7.3
resolution: "semver@npm:7.7.3"
bin:
semver: bin/semver.js
checksum: 10/7a24cffcaa13f53c09ce55e05efe25cd41328730b2308678624f8b9f5fc3093fc4d189f47950f0b811ff8f3c3039c24a2c36717ba7961615c682045bf03e1dda
checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9
languageName: node
linkType: hard