mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-21 08:47:26 +00:00
Compare commits
5 Commits
renovate/c
...
add-device
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9331282521 | ||
|
|
9299b84708 | ||
|
|
df7a36e743 | ||
|
|
5786fe4b8d | ||
|
|
6fa274e4bf |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.11.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
|
||||
11
package.json
11
package.json
@@ -34,7 +34,7 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.8",
|
||||
"@codemirror/view": "6.38.6",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||
@@ -115,7 +115,7 @@
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -194,7 +194,7 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "12.0.0",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -233,10 +233,9 @@
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.5.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.11.0",
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
floors: {
|
||||
id: string;
|
||||
areas: string[];
|
||||
}[];
|
||||
areas: string[];
|
||||
}
|
||||
|
||||
export const getAreasFloorHierarchy = (
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[]
|
||||
): AreasFloorHierarchy => {
|
||||
const floorAreas = new Map<string, string[]>();
|
||||
const unassignedAreas: string[] = [];
|
||||
|
||||
for (const area of areas) {
|
||||
if (area.floor_id) {
|
||||
if (!floorAreas.has(area.floor_id)) {
|
||||
floorAreas.set(area.floor_id, []);
|
||||
}
|
||||
floorAreas.get(area.floor_id)!.push(area.area_id);
|
||||
} else {
|
||||
unassignedAreas.push(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
const hierarchy: AreasFloorHierarchy = {
|
||||
floors: floors.map((floor) => ({
|
||||
id: floor.floor_id,
|
||||
areas: floorAreas.get(floor.floor_id) || [],
|
||||
})),
|
||||
areas: unassignedAreas,
|
||||
};
|
||||
|
||||
return hierarchy;
|
||||
};
|
||||
|
||||
export const getAreasOrder = (hierarchy: AreasFloorHierarchy): string[] => {
|
||||
const order: string[] = [];
|
||||
|
||||
for (const floor of hierarchy.floors) {
|
||||
order.push(...floor.areas);
|
||||
}
|
||||
order.push(...hierarchy.areas);
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
export const getFloorOrder = (hierarchy: AreasFloorHierarchy): string[] =>
|
||||
hierarchy.floors.map((floor) => floor.id);
|
||||
@@ -1,67 +0,0 @@
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { canOverrideAlphanumericInput } from "../dom/can-override-input";
|
||||
|
||||
/**
|
||||
* A function to handle a keyboard shortcut.
|
||||
*/
|
||||
export type ShortcutHandler = (event: KeyboardEvent) => void;
|
||||
|
||||
/**
|
||||
* Configuration for a keyboard shortcut.
|
||||
*/
|
||||
export interface ShortcutConfig {
|
||||
handler: ShortcutHandler;
|
||||
/**
|
||||
* If true, allows shortcuts even when text is selected.
|
||||
* Default is false to avoid interrupting copy/paste.
|
||||
*/
|
||||
allowWhenTextSelected?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register keyboard shortcuts using tinykeys.
|
||||
* Automatically blocks shortcuts in input fields and during text selection.
|
||||
*/
|
||||
function registerShortcuts(
|
||||
shortcuts: Record<string, ShortcutConfig>
|
||||
): () => void {
|
||||
const wrappedShortcuts: Record<string, ShortcutHandler> = {};
|
||||
|
||||
Object.entries(shortcuts).forEach(([key, config]) => {
|
||||
wrappedShortcuts[key] = (event: KeyboardEvent) => {
|
||||
if (!canOverrideAlphanumericInput(event.composedPath())) {
|
||||
return;
|
||||
}
|
||||
if (!config.allowWhenTextSelected && window.getSelection()?.toString()) {
|
||||
return;
|
||||
}
|
||||
config.handler(event);
|
||||
};
|
||||
});
|
||||
|
||||
return tinykeys(window, wrappedShortcuts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages keyboard shortcuts registration and cleanup.
|
||||
*/
|
||||
export class ShortcutManager {
|
||||
private _disposer?: () => void;
|
||||
|
||||
/**
|
||||
* Register keyboard shortcuts.
|
||||
* Uses tinykeys syntax: https://github.com/jamiebuilds/tinykeys#usage
|
||||
*/
|
||||
public add(shortcuts: Record<string, ShortcutConfig>) {
|
||||
this._disposer?.();
|
||||
this._disposer = registerShortcuts(shortcuts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all registered shortcuts.
|
||||
*/
|
||||
public remove() {
|
||||
this._disposer?.();
|
||||
this._disposer = undefined;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--_label-text-font: var(--ha-font-family-body);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -298,18 +298,6 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
if (properties.has("data")) {
|
||||
// Clean up checked rows that no longer exist in the data
|
||||
if (this._checkedRows.length) {
|
||||
const validIds = new Set(this.data.map((row) => String(row[this.id])));
|
||||
const validCheckedRows = this._checkedRows.filter((id) =>
|
||||
validIds.has(id)
|
||||
);
|
||||
if (validCheckedRows.length !== this._checkedRows.length) {
|
||||
this._checkedRows = validCheckedRows;
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this._checkableRowsCount = this.data.filter(
|
||||
(row) => row.selectable !== false
|
||||
).length;
|
||||
|
||||
@@ -94,12 +94,6 @@ export class HaDateInput extends LitElement {
|
||||
}
|
||||
|
||||
private _keyDown(ev: KeyboardEvent) {
|
||||
if (["Space", "Enter"].includes(ev.code)) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._openDialog();
|
||||
return;
|
||||
}
|
||||
if (!this.canClear) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,10 +60,6 @@ class HaHLSPlayer extends LitElement {
|
||||
private static streamCount = 0;
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -175,10 +175,10 @@ export class HaMdDialog extends Dialog {
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
margin-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
margin-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
margin-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -45,7 +44,7 @@ const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
panel.url_path === hass.defaultPanel
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
|
||||
@@ -33,7 +33,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-checkbox";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-markdown";
|
||||
import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-service-section-icon";
|
||||
@@ -685,14 +684,10 @@ export class HaServiceControl extends LitElement {
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
><ha-markdown
|
||||
breaks
|
||||
allow-svg
|
||||
.content=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}
|
||||
></ha-markdown>
|
||||
</span>
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${this.disabled ||
|
||||
|
||||
@@ -33,7 +33,6 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -143,7 +142,7 @@ const defaultPanelSorter = (
|
||||
export const computePanels = memoizeOne(
|
||||
(
|
||||
panels: HomeAssistant["panels"],
|
||||
defaultPanel: string,
|
||||
defaultPanel: HomeAssistant["defaultPanel"],
|
||||
panelsOrder: string[],
|
||||
hiddenPanels: string[],
|
||||
locale: HomeAssistant["locale"]
|
||||
@@ -299,8 +298,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.userData !== oldHass.userData ||
|
||||
hass.systemData !== oldHass.systemData ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||
hass.connected !== oldHass.connected
|
||||
);
|
||||
}
|
||||
@@ -403,11 +401,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels,
|
||||
this.hass.locale
|
||||
@@ -422,27 +418,23 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -62,10 +62,6 @@ class HaWebRtcPlayer extends LitElement {
|
||||
private _candidatesList: RTCIceCandidate[] = [];
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
// video is playing in picture-in-picture mode, don't do anything
|
||||
return;
|
||||
}
|
||||
if (document.hidden) {
|
||||
this._cleanUp();
|
||||
} else {
|
||||
|
||||
@@ -34,7 +34,6 @@ class SearchInput extends LitElement {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
snapshots?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getAreasFloorHierarchy } from "../common/areas/areas-floor-hierarchy";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
@@ -13,7 +12,11 @@ import {
|
||||
} from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import type { FloorRegistryEntry } from "./floor_registry";
|
||||
import {
|
||||
floorCompare,
|
||||
getFloorAreaLookup,
|
||||
type FloorRegistryEntry,
|
||||
} from "./floor_registry";
|
||||
|
||||
export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
type: "floor" | "area";
|
||||
@@ -179,59 +182,68 @@ export const getAreasAndFloors = (
|
||||
);
|
||||
}
|
||||
|
||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||
const unassignedAreas = Object.values(outputAreas).filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
|
||||
const compare = floorCompare(haFloors);
|
||||
|
||||
// @ts-ignore
|
||||
const floorAreaEntries: [
|
||||
FloorRegistryEntry | undefined,
|
||||
AreaRegistryEntry[],
|
||||
][] = Object.entries(floorAreaLookup)
|
||||
.map(([floorId, floorAreas]) => {
|
||||
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||
return [floor, floorAreas] as const;
|
||||
})
|
||||
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
|
||||
|
||||
const items: FloorComboBoxItem[] = [];
|
||||
|
||||
hierarchy.floors.forEach((f) => {
|
||||
const floor = haFloors[f.id];
|
||||
const floorAreas = f.areas.map((areaId) => haAreas[areaId]);
|
||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
const floorName = computeFloorName(floor);
|
||||
|
||||
const areaSearchLabels = floorAreas
|
||||
.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return [area.area_id, ...(areaName ? [areaName] : []), ...area.aliases];
|
||||
})
|
||||
.flat();
|
||||
|
||||
items.push({
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
});
|
||||
const areaSearchLabels = floorAreas
|
||||
.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return [area.area_id, areaName, ...area.aliases];
|
||||
})
|
||||
.flat();
|
||||
|
||||
items.push({
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
});
|
||||
}
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
type: "area" as const,
|
||||
primary: areaName || area.area_id,
|
||||
primary: areaName,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [
|
||||
area.area_id,
|
||||
...(areaName ? [areaName] : []),
|
||||
...area.aliases,
|
||||
],
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
items.push(
|
||||
...hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
...unassignedAreas.map((area) => {
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
|
||||
@@ -59,15 +59,6 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
||||
area_id: areaId,
|
||||
});
|
||||
|
||||
export const reorderAreaRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
areaIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/area_registry/reorder",
|
||||
area_ids: areaIds,
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): AreaEntityLookup => {
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface CalendarEventData {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEventMutableParams {
|
||||
@@ -40,7 +39,6 @@ export interface CalendarEventMutableParams {
|
||||
dtend: string;
|
||||
rrule?: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
// The scope of a delete/update for a recurring event
|
||||
@@ -98,7 +96,6 @@ export const fetchCalendarEvents = async (
|
||||
uid: ev.uid,
|
||||
summary: ev.summary,
|
||||
description: ev.description,
|
||||
location: ev.location,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
recurrence_id: ev.recurrence_id,
|
||||
|
||||
@@ -51,15 +51,6 @@ export const deleteFloorRegistryEntry = (
|
||||
floor_id: floorId,
|
||||
});
|
||||
|
||||
export const reorderFloorRegistryEntries = (
|
||||
hass: HomeAssistant,
|
||||
floorIds: string[]
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/floor_registry/reorder",
|
||||
floor_ids: floorIds,
|
||||
});
|
||||
|
||||
export const getFloorAreaLookup = (
|
||||
areas: AreaRegistryEntry[]
|
||||
): FloorAreaLookup => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
@@ -11,24 +10,15 @@ export interface SidebarFrontendUserData {
|
||||
hiddenPanels: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
defaultPanel?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
core: CoreFrontendUserData;
|
||||
sidebar: SidebarFrontendUserData;
|
||||
}
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidUserDataKey = keyof FrontendUserData;
|
||||
|
||||
export type ValidSystemDataKey = keyof FrontendSystemData;
|
||||
|
||||
export const fetchFrontendUserData = async <
|
||||
UserDataKey extends ValidUserDataKey,
|
||||
>(
|
||||
@@ -69,46 +59,3 @@ export const subscribeFrontendUserData = <UserDataKey extends ValidUserDataKey>(
|
||||
key: userDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey
|
||||
): Promise<FrontendSystemData[SystemDataKey] | null> => {
|
||||
const result = await conn.sendMessagePromise<{
|
||||
value: FrontendSystemData[SystemDataKey] | null;
|
||||
}>({
|
||||
type: "frontend/get_system_data",
|
||||
key,
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
export const saveFrontendSystemData = async <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
key: SystemDataKey,
|
||||
value: FrontendSystemData[SystemDataKey]
|
||||
): Promise<void> =>
|
||||
conn.sendMessagePromise<undefined>({
|
||||
type: "frontend/set_system_data",
|
||||
key,
|
||||
value,
|
||||
});
|
||||
|
||||
export const subscribeFrontendSystemData = <
|
||||
SystemDataKey extends ValidSystemDataKey,
|
||||
>(
|
||||
conn: Connection,
|
||||
systemDataKey: SystemDataKey,
|
||||
onChange: (data: { value: FrontendSystemData[SystemDataKey] | null }) => void
|
||||
) =>
|
||||
conn.subscribeMessage<{ value: FrontendSystemData[SystemDataKey] | null }>(
|
||||
onChange,
|
||||
{
|
||||
type: "frontend/subscribe_system_data",
|
||||
key: systemDataKey,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
export const getLegacyDefaultPanelUrlPath = (): string | null => {
|
||||
export const getStorageDefaultPanelUrlPath = (): string => {
|
||||
const defaultPanel = window.localStorage.getItem("defaultPanel");
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : null;
|
||||
|
||||
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
|
||||
};
|
||||
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.defaultPanel ||
|
||||
hass.systemData?.defaultPanel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
|
||||
const panel = getDefaultPanelUrlPath(hass);
|
||||
|
||||
return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL];
|
||||
export const setDefaultPanel = (
|
||||
element: HTMLElement,
|
||||
urlPath: string
|
||||
): void => {
|
||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
||||
};
|
||||
|
||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||
hass.panels[hass.defaultPanel]
|
||||
? hass.panels[hass.defaultPanel]
|
||||
: hass.panels[DEFAULT_PANEL];
|
||||
|
||||
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
||||
if (panel.url_path === "lovelace") {
|
||||
return "panel.states" as const;
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "../../data/frontend";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -95,11 +94,9 @@ class DialogEditSidebar extends LitElement {
|
||||
|
||||
const panels = this._panels(this.hass.panels);
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
defaultPanel,
|
||||
this.hass.defaultPanel,
|
||||
this._order,
|
||||
this._hidden,
|
||||
this.hass.locale
|
||||
@@ -123,12 +120,12 @@ class DialogEditSidebar extends LitElement {
|
||||
].map((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
panel.url_path === this.hass.defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -46,13 +46,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
|
||||
return html`
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : undefined}
|
||||
.direction=${computeRTLDirection(this.hass)}
|
||||
@MDCDrawer:closed=${this._drawerClosed}
|
||||
>
|
||||
@@ -62,14 +59,12 @@ export class HomeAssistantMain extends LitElement {
|
||||
.route=${this.route}
|
||||
.alwaysExpand=${sidebarNarrow || this.hass.dockedSidebar === "docked"}
|
||||
></ha-sidebar>
|
||||
${isPanelReady
|
||||
? html`<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>`
|
||||
: nothing}
|
||||
<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
></partial-panel-resolver>
|
||||
</ha-drawer>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { getStorageDefaultPanelUrlPath } from "../data/panel";
|
||||
import type { WindowWithPreloads } from "../data/preloads";
|
||||
import type { RecorderInfo } from "../data/recorder";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "../util/register-service-worker";
|
||||
import "./ha-init-page";
|
||||
import "./home-assistant-main";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
|
||||
const useHash = __DEMO__;
|
||||
const curPath = () =>
|
||||
@@ -52,6 +53,11 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
super();
|
||||
const path = curPath();
|
||||
|
||||
if (["", "/"].includes(path)) {
|
||||
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
this._route = {
|
||||
prefix: "",
|
||||
path,
|
||||
|
||||
@@ -80,12 +80,10 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
${this._data!.rrule
|
||||
? this._renderRRuleAsText(this._data.rrule)
|
||||
: ""}
|
||||
${this._data.location
|
||||
? html`${this._data.location} <br />`
|
||||
: nothing}
|
||||
${this._data.description
|
||||
? html`<br />
|
||||
<div class="description">${this._data.description}</div>`
|
||||
<div class="description">${this._data.description}</div>
|
||||
<br />`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +241,7 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
haStyleDialog,
|
||||
css`
|
||||
state-info {
|
||||
margin-top: 24px;
|
||||
line-height: 40px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
width: 40px;
|
||||
|
||||
@@ -63,8 +63,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _description? = "";
|
||||
|
||||
@state() private _location? = "";
|
||||
|
||||
@state() private _rrule?: string;
|
||||
|
||||
@state() private _allDay = false;
|
||||
@@ -81,8 +79,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
// timezone, but floating without a timezone.
|
||||
private _timeZone?: string;
|
||||
|
||||
private _hasLocation = false;
|
||||
|
||||
public showDialog(params: CalendarEventEditDialogParams): void {
|
||||
this._error = undefined;
|
||||
this._info = undefined;
|
||||
@@ -103,10 +99,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
this._summary = entry.summary;
|
||||
this._description = entry.description;
|
||||
if (entry.location) {
|
||||
this._hasLocation = true;
|
||||
this._location = entry.location;
|
||||
}
|
||||
this._rrule = entry.rrule;
|
||||
if (this._allDay) {
|
||||
this._dtstart = new Date(entry.dtstart + "T00:00:00");
|
||||
@@ -138,8 +130,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._dtend = undefined;
|
||||
this._summary = "";
|
||||
this._description = "";
|
||||
this._location = "";
|
||||
this._hasLocation = false;
|
||||
this._rrule = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -191,15 +181,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
class="location"
|
||||
name="location"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.calendar.event.location"
|
||||
)}
|
||||
.value=${this._location}
|
||||
@change=${this._handleLocationChanged}
|
||||
></ha-textfield>
|
||||
<ha-textarea
|
||||
class="description"
|
||||
name="description"
|
||||
@@ -345,10 +326,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._description = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleLocationChanged(ev: Event) {
|
||||
this._location = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleRRuleChanged(ev) {
|
||||
this._rrule = ev.detail.value;
|
||||
}
|
||||
@@ -422,7 +399,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
const data: CalendarEventMutableParams = {
|
||||
summary: this._summary,
|
||||
description: this._description,
|
||||
location: this._location || (this._hasLocation ? "" : undefined),
|
||||
rrule: this._rrule || undefined,
|
||||
dtstart: "",
|
||||
dtend: "",
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
computeAreaTileCardConfig,
|
||||
getAreas,
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
|
||||
export interface ClimateViewStrategyConfig {
|
||||
type: "climate";
|
||||
@@ -135,9 +139,9 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
_config: ClimateViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
@@ -149,11 +153,10 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
|
||||
const entities = findEntities(allEntities, climateFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
@@ -182,7 +185,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
if (home.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
@@ -197,7 +200,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForClimate(hierarchy.areas, hass, entities);
|
||||
const areaCards = processAreasForClimate(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
|
||||
@@ -2,47 +2,38 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiHelpCircle,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
getAreasFloorHierarchy,
|
||||
getAreasOrder,
|
||||
getFloorOrder,
|
||||
type AreasFloorHierarchy,
|
||||
} from "../../../common/areas/areas-floor-hierarchy";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
createAreaRegistryEntry,
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import {
|
||||
createFloorRegistryEntry,
|
||||
deleteFloorRegistryEntry,
|
||||
reorderFloorRegistryEntries,
|
||||
getFloorAreaLookup,
|
||||
updateFloorRegistryEntry,
|
||||
} from "../../../data/floor_registry";
|
||||
import {
|
||||
@@ -51,7 +42,6 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
@@ -62,17 +52,7 @@ import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-deta
|
||||
|
||||
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
const SORT_OPTIONS: HaSortableOptions = {
|
||||
sort: true,
|
||||
delay: 500,
|
||||
delayOnTouchOnly: true,
|
||||
};
|
||||
|
||||
interface AreaStats {
|
||||
devices: number;
|
||||
services: number;
|
||||
entities: number;
|
||||
}
|
||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@@ -84,50 +64,55 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
@state() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _blockHierarchyUpdate = false;
|
||||
|
||||
private _blockHierarchyUpdateTimeout?: number;
|
||||
|
||||
private _processAreasStats = memoizeOne(
|
||||
private _processAreas = memoizeOne(
|
||||
(
|
||||
areas: HomeAssistant["areas"],
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"]
|
||||
): Map<string, AreaStats> => {
|
||||
const computeAreaStats = (area: AreaRegistryEntry) => {
|
||||
let devicesCount = 0;
|
||||
let servicesCount = 0;
|
||||
let entitiesCount = 0;
|
||||
entities: HomeAssistant["entities"],
|
||||
floors: HomeAssistant["floors"]
|
||||
) => {
|
||||
const processArea = (area: AreaRegistryEntry) => {
|
||||
let noDevicesInArea = 0;
|
||||
let noServicesInArea = 0;
|
||||
let noEntitiesInArea = 0;
|
||||
|
||||
for (const device of Object.values(devices)) {
|
||||
if (device.area_id === area.area_id) {
|
||||
if (device.entry_type === "service") {
|
||||
servicesCount++;
|
||||
noServicesInArea++;
|
||||
} else {
|
||||
devicesCount++;
|
||||
noDevicesInArea++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const entity of Object.values(entities)) {
|
||||
if (entity.area_id === area.area_id) {
|
||||
entitiesCount++;
|
||||
noEntitiesInArea++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
devices: devicesCount,
|
||||
services: servicesCount,
|
||||
entities: entitiesCount,
|
||||
...area,
|
||||
devices: noDevicesInArea,
|
||||
services: noServicesInArea,
|
||||
entities: noEntitiesInArea,
|
||||
};
|
||||
};
|
||||
const areaStats = new Map<string, AreaStats>();
|
||||
Object.values(areas).forEach((area) => {
|
||||
areaStats.set(area.area_id, computeAreaStats(area));
|
||||
});
|
||||
return areaStats;
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
const unassignedAreas = areas.filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
floors: Object.values(floors).map((floor) => ({
|
||||
...floor,
|
||||
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
||||
})),
|
||||
unassignedAreas: unassignedAreas.map(processArea),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -135,32 +120,25 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("hass")) {
|
||||
const oldHass = changedProperties.get("hass");
|
||||
if (
|
||||
(this.hass.areas !== oldHass?.areas ||
|
||||
this.hass.floors !== oldHass?.floors) &&
|
||||
!this._blockHierarchyUpdate
|
||||
) {
|
||||
this._computeHierarchy();
|
||||
if (this.hass.areas !== oldHass?.areas) {
|
||||
this._areas = Object.values(this.hass.areas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _computeHierarchy() {
|
||||
this._hierarchy = getAreasFloorHierarchy(
|
||||
Object.values(this.hass.floors),
|
||||
Object.values(this.hass.areas)
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult<1> | typeof nothing {
|
||||
if (!this._hierarchy) {
|
||||
return nothing;
|
||||
}
|
||||
const areasStats = this._processAreasStats(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities
|
||||
);
|
||||
protected render(): TemplateResult {
|
||||
const areasAndFloors =
|
||||
!this.hass.areas ||
|
||||
!this.hass.devices ||
|
||||
!this.hass.entities ||
|
||||
!this.hass.floors
|
||||
? undefined
|
||||
: this._processAreas(
|
||||
this._areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -179,120 +157,81 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<div class="container">
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".floor"
|
||||
@item-moved=${this._floorMoved}
|
||||
.options=${SORT_OPTIONS}
|
||||
group="floors"
|
||||
invert-swap
|
||||
>
|
||||
<div class="floors">
|
||||
${this._hierarchy.floors.map(({ areas, id }) => {
|
||||
const floor = this.hass.floors[id];
|
||||
if (!floor) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
${floor.name}
|
||||
</h2>
|
||||
<div class="actions">
|
||||
<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
@item-moved=${this._areaMoved}
|
||||
group="areas"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${floor.floor_id}
|
||||
>
|
||||
<div class="areas">
|
||||
${areas.map((areaId) => {
|
||||
const area = this.hass.areas[areaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
}
|
||||
const stats = areasStats.get(area.area_id);
|
||||
return this._renderArea(area, stats);
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
|
||||
${this._hierarchy.areas.length
|
||||
? html`
|
||||
<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.unassigned_areas"
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
@item-moved=${this._areaMoved}
|
||||
group="areas"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${UNASSIGNED_FLOOR}
|
||||
${areasAndFloors?.floors.map(
|
||||
(floor) =>
|
||||
html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
${floor.name}
|
||||
</h2>
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<div class="areas">
|
||||
${this._hierarchy.areas.map((areaId) => {
|
||||
const area = this.hass.areas[areaId];
|
||||
if (!area) {
|
||||
return nothing;
|
||||
}
|
||||
const stats = areasStats.get(area.area_id);
|
||||
return this._renderArea(area, stats);
|
||||
})}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
`
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${floor.floor_id}
|
||||
>
|
||||
<div class="areas">
|
||||
${floor.areas.map((area) => this._renderArea(area))}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
)}
|
||||
${areasAndFloors?.unassignedAreas.length
|
||||
? html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.unassigned_areas"
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.floor=${UNASSIGNED_FLOOR}
|
||||
>
|
||||
<div class="areas">
|
||||
${areasAndFloors?.unassignedAreas.map((area) =>
|
||||
this._renderArea(area)
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
@@ -320,60 +259,56 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderArea(
|
||||
area: AreaRegistryEntry,
|
||||
stats: AreaStats | undefined
|
||||
): TemplateResult<1> {
|
||||
return html`
|
||||
<a href=${`/config/areas/area/${area.area_id}`} .sortableData=${area}>
|
||||
<ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture
|
||||
? `url(${area.picture})`
|
||||
: undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
>
|
||||
${!area.picture && area.icon
|
||||
? html`<ha-icon .icon=${area.icon}></ha-icon>`
|
||||
: ""}
|
||||
private _renderArea(area) {
|
||||
return html`<a
|
||||
href=${`/config/areas/area/${area.area_id}`}
|
||||
.sortableData=${area}
|
||||
>
|
||||
<ha-card outlined>
|
||||
<div
|
||||
style=${styleMap({
|
||||
backgroundImage: area.picture ? `url(${area.picture})` : undefined,
|
||||
})}
|
||||
class="picture ${!area.picture ? "placeholder" : ""}"
|
||||
>
|
||||
${!area.picture && area.icon
|
||||
? html`<ha-icon .icon=${area.icon}></ha-icon>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="card-header">
|
||||
${area.name}
|
||||
<ha-icon-button
|
||||
.area=${area}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._openAreaDetails}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${formatListWithAnds(
|
||||
this.hass.locale,
|
||||
[
|
||||
area.devices &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
{ count: area.devices }
|
||||
),
|
||||
area.services &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
{ count: area.services }
|
||||
),
|
||||
area.entities &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: area.entities }
|
||||
),
|
||||
].filter((v): v is string => Boolean(v))
|
||||
)}
|
||||
</div>
|
||||
<div class="card-header">
|
||||
${area.name}
|
||||
<ha-icon-button
|
||||
.area=${area}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._openAreaDetails}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div>
|
||||
${formatListWithAnds(
|
||||
this.hass.locale,
|
||||
[
|
||||
stats?.devices &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.devices",
|
||||
{ count: stats.devices }
|
||||
),
|
||||
stats?.services &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.services",
|
||||
{ count: stats.services }
|
||||
),
|
||||
stats?.entities &&
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: stats.entities }
|
||||
),
|
||||
].filter((v): v is string => Boolean(v))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</a>
|
||||
`;
|
||||
</div>
|
||||
</ha-card>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
@@ -391,170 +326,24 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _floorMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const reorderFloors = (
|
||||
floors: AreasFloorHierarchy["floors"],
|
||||
oldIdx: number,
|
||||
newIdx: number
|
||||
) => {
|
||||
const newFloors = [...floors];
|
||||
const [movedFloor] = newFloors.splice(oldIdx, 1);
|
||||
newFloors.splice(newIdx, 0, movedFloor);
|
||||
return newFloors;
|
||||
};
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex),
|
||||
};
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
const floorOrder = getFloorOrder(this._hierarchy);
|
||||
|
||||
// Block hierarchy updates for 500ms to avoid flickering
|
||||
// because of multiple async updates
|
||||
this._blockHierarchyUpdateFor(500);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
await reorderFloorRegistryEntries(this.hass, floorOrder);
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor_reorder_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { floor } = ev.currentTarget;
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const floorId = floor === UNASSIGNED_FLOOR ? null : floor;
|
||||
|
||||
// Reorder areas within the same floor
|
||||
const reorderAreas = (areas: string[], oldIdx: number, newIdx: number) => {
|
||||
const newAreas = [...areas];
|
||||
const [movedArea] = newAreas.splice(oldIdx, 1);
|
||||
newAreas.splice(newIdx, 0, movedArea);
|
||||
return newAreas;
|
||||
};
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: this._hierarchy.floors.map((f) => {
|
||||
if (f.id === floorId) {
|
||||
return {
|
||||
...f,
|
||||
areas: reorderAreas(f.areas, oldIndex, newIndex),
|
||||
};
|
||||
}
|
||||
return f;
|
||||
}),
|
||||
areas:
|
||||
floorId === null
|
||||
? reorderAreas(this._hierarchy.areas, oldIndex, newIndex)
|
||||
: this._hierarchy.areas,
|
||||
};
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.area_move_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaAdded(ev) {
|
||||
ev.stopPropagation();
|
||||
if (!this.hass || !this._hierarchy) {
|
||||
return;
|
||||
}
|
||||
const { floor } = ev.currentTarget;
|
||||
const { data: area, index } = ev.detail;
|
||||
|
||||
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
|
||||
|
||||
// Insert area at the specified index
|
||||
const insertAtIndex = (areas: string[], areaId: string, idx: number) => {
|
||||
const newAreas = [...areas];
|
||||
newAreas.splice(idx, 0, areaId);
|
||||
return newAreas;
|
||||
};
|
||||
const { data: area } = ev.detail;
|
||||
|
||||
// Optimistically update UI
|
||||
this._hierarchy = {
|
||||
...this._hierarchy,
|
||||
floors: this._hierarchy.floors.map((f) => {
|
||||
if (f.id === newFloorId) {
|
||||
return {
|
||||
...f,
|
||||
areas: insertAtIndex(f.areas, area.area_id, index),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...f,
|
||||
areas: f.areas.filter((id) => id !== area.area_id),
|
||||
};
|
||||
}),
|
||||
areas:
|
||||
newFloorId === null
|
||||
? insertAtIndex(this._hierarchy.areas, area.area_id, index)
|
||||
: this._hierarchy.areas.filter((id) => id !== area.area_id),
|
||||
};
|
||||
this._areas = this._areas.map<AreaRegistryEntry>((a) => {
|
||||
if (a.area_id === area.area_id) {
|
||||
return { ...a, floor_id: newFloorId };
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
||||
const areaOrder = getAreasOrder(this._hierarchy);
|
||||
|
||||
// Block hierarchy updates for 500ms to avoid flickering
|
||||
// because of multiple async updates
|
||||
this._blockHierarchyUpdateFor(500);
|
||||
|
||||
try {
|
||||
await reorderAreaRegistryEntries(this.hass, areaOrder);
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id: newFloorId,
|
||||
});
|
||||
} catch {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.area_move_failed"
|
||||
),
|
||||
});
|
||||
// Revert on error
|
||||
this._computeHierarchy();
|
||||
}
|
||||
}
|
||||
|
||||
private _blockHierarchyUpdateFor(time: number) {
|
||||
this._blockHierarchyUpdate = true;
|
||||
if (this._blockHierarchyUpdateTimeout) {
|
||||
window.clearTimeout(this._blockHierarchyUpdateTimeout);
|
||||
}
|
||||
this._blockHierarchyUpdateTimeout = window.setTimeout(() => {
|
||||
this._blockHierarchyUpdate = false;
|
||||
}, time);
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id: newFloorId,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
|
||||
@@ -674,10 +463,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.header ha-icon {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.header .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.areas {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
@@ -688,10 +473,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
.areas > * {
|
||||
max-width: 500px;
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1336,7 +1336,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-10);
|
||||
}
|
||||
ha-bottom-sheet .groups {
|
||||
@@ -1400,7 +1400,7 @@ class DialogAddAutomationElement
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -1161,9 +1161,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
private async _delete(automation) {
|
||||
try {
|
||||
await deleteAutomation(this.hass, automation.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== automation.entity_id
|
||||
);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-analytics";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { Analytics } from "../../../data/analytics";
|
||||
import {
|
||||
getAnalyticsDetails,
|
||||
@@ -17,6 +13,8 @@ import {
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
|
||||
@customElement("ha-config-analytics")
|
||||
class ConfigAnalytics extends LitElement {
|
||||
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||
"Home Assistant analytics"}
|
||||
>
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
||||
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.analytics.intro")}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-analytics
|
||||
translation_key_panel="config"
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
@@ -45,26 +55,50 @@ class ConfigAnalytics extends LitElement {
|
||||
.analytics=${this._analyticsDetails}
|
||||
></ha-analytics>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
||||
</ha-button>
|
||||
</div>
|
||||
${isDevVersion(this.hass.config.version)
|
||||
? html`<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/device-database/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.learn_more"
|
||||
)}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._handleDeviceRowClick}
|
||||
.checked=${!!this._analyticsDetails?.preferences.snapshots}
|
||||
.disabled=${this._analyticsDetails === undefined}
|
||||
name="snapshots"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -96,11 +130,25 @@ class ConfigAnalytics extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDeviceRowClick(ev: Event) {
|
||||
const target = ev.target as HaSwitch;
|
||||
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: {
|
||||
...this._analyticsDetails!.preferences,
|
||||
snapshots: target.checked,
|
||||
},
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -117,21 +165,10 @@ class ConfigAnalytics extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
ha-card:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-button[size="small"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
${!this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
@click=${this._handleEditDeviceButton}
|
||||
@click=${this._handleEditDevice}
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.edit"
|
||||
@@ -106,7 +106,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${this.narrow
|
||||
? html`<ha-md-menu-item .clickAction=${this._handleEditDevice}>
|
||||
? html`<ha-md-menu-item @click=${this._handleEditDevice}>
|
||||
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.device.edit"
|
||||
@@ -115,7 +115,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
: nothing}
|
||||
${entities.length
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleNavigateToEntities}>
|
||||
<ha-md-menu-item @click=${this._handleNavigateToEntities}>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="start"
|
||||
@@ -130,7 +130,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
: nothing}
|
||||
<ha-md-menu-item
|
||||
class=${device.disabled_by !== "user" ? "warning" : ""}
|
||||
.clickAction=${this._handleDisableDevice}
|
||||
@click=${this._handleDisableDevice}
|
||||
.disabled=${device.disabled_by !== "user" && device.disabled_by}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStopCircleOutline} slot="start"></ha-svg-icon>
|
||||
@@ -160,7 +160,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
${this.entry.supports_remove_device
|
||||
? html`<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDeleteDevice}
|
||||
@click=${this._handleDeleteDevice}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
@@ -175,25 +175,21 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
private _getEntities = (): EntityRegistryEntry[] =>
|
||||
this.entities?.filter((entity) => entity.device_id === this.device.id);
|
||||
|
||||
private _handleEditDeviceButton(ev: MouseEvent) {
|
||||
private _handleEditDevice(ev: MouseEvent) {
|
||||
ev.stopPropagation(); // Prevent triggering the click handler on the list item
|
||||
this._handleEditDevice();
|
||||
}
|
||||
|
||||
private _handleEditDevice = () => {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this.device,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, updates);
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleNavigateToEntities = () => {
|
||||
private _handleNavigateToEntities() {
|
||||
navigate(`/config/entities/?historyBack=1&device=${this.device.id}`);
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDisableDevice = async () => {
|
||||
private async _handleDisableDevice() {
|
||||
const disable = this.device.disabled_by === null;
|
||||
|
||||
if (disable) {
|
||||
@@ -267,9 +263,9 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
await updateDeviceRegistryEntry(this.hass, this.device.id, {
|
||||
disabled_by: disable ? "user" : null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDeleteDevice = async () => {
|
||||
private async _handleDeleteDevice() {
|
||||
const entry = this.entry;
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.panel.config.devices.confirm_delete"),
|
||||
@@ -294,7 +290,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleNavigateToDevice() {
|
||||
navigate(`/config/devices/device/${this.device.id}`);
|
||||
|
||||
@@ -302,7 +302,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleReload}>
|
||||
<ha-md-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
@@ -311,14 +311,14 @@ class HaConfigEntryRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._handleRename} graphic="icon">
|
||||
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
|
||||
<ha-md-menu-item .clickAction=${this._handleCopy} graphic="icon">
|
||||
<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"
|
||||
@@ -328,7 +328,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
${Object.keys(item.supported_subentry_types).map(
|
||||
(flowType) =>
|
||||
html`<ha-md-menu-item
|
||||
.clickAction=${this._addSubEntry}
|
||||
@click=${this._addSubEntry}
|
||||
.entry=${item}
|
||||
.flowType=${flowType}
|
||||
graphic="icon"
|
||||
@@ -360,7 +360,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleReconfigure}>
|
||||
<ha-md-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
@@ -369,10 +369,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-menu-item
|
||||
.clickAction=${this._handleSystemOptions}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCogOutline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
@@ -380,7 +377,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
</ha-md-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`
|
||||
<ha-md-menu-item .clickAction=${this._handleEnable}>
|
||||
<ha-md-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
@@ -392,7 +389,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDisable}
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -406,10 +403,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDelete}
|
||||
>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
@@ -617,7 +611,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleReload = async () => {
|
||||
private async _handleReload() {
|
||||
const result = await reloadConfigEntry(this.hass, this.entry.entry_id);
|
||||
const locale_key = result.require_restart
|
||||
? "reload_restart_confirm"
|
||||
@@ -627,9 +621,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
`ui.panel.config.integrations.config_entry.${locale_key}`
|
||||
),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleReconfigure = async () => {
|
||||
private async _handleReconfigure() {
|
||||
showConfigFlowDialog(this, {
|
||||
startFlowHandler: this.entry.domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
@@ -637,18 +631,18 @@ class HaConfigEntryRow extends LitElement {
|
||||
entryId: this.entry.entry_id,
|
||||
navigateToResult: true,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleCopy = async () => {
|
||||
private async _handleCopy() {
|
||||
await copyToClipboard(this.entry.entry_id);
|
||||
showToast(this, {
|
||||
message:
|
||||
this.hass?.localize("ui.common.copied_clipboard") ||
|
||||
"Copied to clipboard",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _handleRename = async () => {
|
||||
private async _handleRename() {
|
||||
const newName = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.integrations.rename_dialog"),
|
||||
defaultValue: this.entry.title,
|
||||
@@ -662,7 +656,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
await updateConfigEntry(this.hass, this.entry.entry_id, {
|
||||
title: newName,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async _signUrl(ev) {
|
||||
const anchor = ev.currentTarget;
|
||||
@@ -674,7 +668,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private _handleDisable = async () => {
|
||||
private async _handleDisable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
@@ -712,9 +706,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleEnable = async () => {
|
||||
private async _handleEnable() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
let result: DisableConfigEntryResult;
|
||||
@@ -737,9 +731,9 @@ class HaConfigEntryRow extends LitElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDelete = async () => {
|
||||
private async _handleDelete() {
|
||||
const entryId = this.entry.entry_id;
|
||||
|
||||
const applicationCredentialsId =
|
||||
@@ -773,20 +767,20 @@ class HaConfigEntryRow extends LitElement {
|
||||
if (applicationCredentialsId) {
|
||||
this._removeApplicationCredential(applicationCredentialsId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _handleSystemOptions = () => {
|
||||
private _handleSystemOptions() {
|
||||
showConfigEntrySystemOptionsDialog(this, {
|
||||
entry: this.entry,
|
||||
manifest: this.manifest,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private _addSubEntry = (item) => {
|
||||
showSubConfigFlowDialog(this, this.entry, item.flowType, {
|
||||
private _addSubEntry(ev) {
|
||||
showSubConfigFlowDialog(this, this.entry, ev.target.flowType, {
|
||||
startFlowHandler: this.entry.entry_id,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
|
||||
@@ -145,16 +145,13 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
</ha-md-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-menu-item .clickAction=${this._handleRenameSub}>
|
||||
<ha-md-menu-item @click=${this._handleRenameSub}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item
|
||||
class="warning"
|
||||
.clickAction=${this._handleDeleteSub}
|
||||
>
|
||||
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
@@ -225,7 +222,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRenameSub = async (): Promise<void> => {
|
||||
private async _handleRenameSub(): Promise<void> {
|
||||
const newName = await showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.common.rename"),
|
||||
defaultValue: this.subEntry.title,
|
||||
@@ -242,9 +239,9 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
this.subEntry.subentry_id,
|
||||
{ title: newName }
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _handleDeleteSub = async (): Promise<void> => {
|
||||
private async _handleDeleteSub(): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
@@ -266,7 +263,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
this.entry.entry_id,
|
||||
this.subEntry.subentry_id
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.expand-button {
|
||||
|
||||
@@ -4,20 +4,18 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,8 +59,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
const defaultPanelUrlPath = this.hass.defaultPanel;
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
return html`
|
||||
@@ -254,38 +251,15 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
private _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
setDefaultPanel(
|
||||
this,
|
||||
urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
|
||||
);
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -287,7 +286,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
);
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
@@ -404,8 +403,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -419,7 +416,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards, defaultPanel)}
|
||||
.data=${this._getItems(this._dashboards, this.hass.defaultPanel)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
|
||||
@@ -1112,9 +1112,6 @@ ${rejected
|
||||
private async _delete(scene: SceneEntity): Promise<void> {
|
||||
if (scene.attributes.id) {
|
||||
await deleteScene(this.hass, scene.attributes.id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== scene.entity_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1183,9 +1183,6 @@ ${rejected
|
||||
);
|
||||
if (entry) {
|
||||
await deleteScript(this.hass, entry.unique_id);
|
||||
this._selected = this._selected.filter(
|
||||
(entityId) => entityId !== script.entity_id
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { mdiPencil, mdiDownload } from "@mdi/js";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -6,7 +6,6 @@ import "../../components/ha-menu-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/ha-alert";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -22,7 +21,6 @@ import type {
|
||||
GasSourceTypeEnergyPreference,
|
||||
WaterSourceTypeEnergyPreference,
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyCollection,
|
||||
} from "../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
@@ -32,28 +30,13 @@ import {
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import type { StatisticValue } from "../../data/recorder";
|
||||
|
||||
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
|
||||
|
||||
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
type: "energy",
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-electricity",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
path: "electricity",
|
||||
},
|
||||
{
|
||||
type: "panel",
|
||||
path: "setup",
|
||||
cards: [{ type: "custom:energy-setup-wizard-card" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -63,30 +46,13 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@property({ attribute: false }) public route?: {
|
||||
path: string;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
private _energyCollection?: EnergyCollection;
|
||||
|
||||
get _viewPath(): string | undefined {
|
||||
const viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
return viewPath ? decodeURI(viewPath) : undefined;
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadPrefs();
|
||||
}
|
||||
|
||||
public async willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
}
|
||||
@@ -96,36 +62,9 @@ class PanelEnergy extends LitElement {
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass?.locale !== this.hass.locale) {
|
||||
this._setLovelace();
|
||||
} else if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._reloadView();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadPrefs() {
|
||||
if (this._viewPath === "setup") {
|
||||
await import("./cards/energy-setup-wizard-card");
|
||||
} else {
|
||||
this._energyCollection = getEnergyDataCollection(this.hass, {
|
||||
key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
});
|
||||
try {
|
||||
// Have to manually refresh here as we don't want to subscribe yet
|
||||
await this._energyCollection.refresh();
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
this._error = err.message;
|
||||
return;
|
||||
}
|
||||
const prefs = this._energyCollection.prefs!;
|
||||
if (
|
||||
prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0
|
||||
) {
|
||||
// No energy sources available, start from scratch
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._reloadView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,33 +73,11 @@ class PanelEnergy extends LitElement {
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._energyCollection?.prefs) {
|
||||
// Still loading
|
||||
return html`<div class="centered">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>`;
|
||||
}
|
||||
const { prefs } = this._energyCollection;
|
||||
const isSingleView = prefs.energy_sources.every((source) =>
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
let viewPath = this._viewPath;
|
||||
if (isSingleView) {
|
||||
// if only electricity sources, show electricity view directly
|
||||
viewPath = "electricity";
|
||||
}
|
||||
const viewIndex = Math.max(
|
||||
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
|
||||
0
|
||||
);
|
||||
const showBack =
|
||||
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="toolbar">
|
||||
${showBack
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
@@ -182,7 +99,7 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
<hui-energy-period-selector
|
||||
.hass=${this.hass}
|
||||
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
|
||||
collection-key="energy_dashboard"
|
||||
>
|
||||
${this.hass.user?.is_admin
|
||||
? html` <ha-list-item
|
||||
@@ -210,21 +127,12 @@ class PanelEnergy extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@reload-energy-panel=${this._reloadView}
|
||||
>
|
||||
${this._error
|
||||
? html`<div class="centered">
|
||||
<ha-alert alert-type="error">
|
||||
An error occurred while fetching your energy preferences:
|
||||
${this._error}
|
||||
</ha-alert>
|
||||
</div>`
|
||||
: this._lovelace
|
||||
? html`<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${viewIndex}
|
||||
></hui-view>`
|
||||
: nothing}
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
@@ -252,7 +160,9 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
private async _dumpCSV(ev) {
|
||||
ev.stopPropagation();
|
||||
const energyData = this._energyCollection!;
|
||||
const energyData = getEnergyDataCollection(this.hass, {
|
||||
key: "energy_dashboard",
|
||||
});
|
||||
|
||||
if (!energyData.prefs || !energyData.state.stats) {
|
||||
return;
|
||||
@@ -549,11 +459,11 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
|
||||
private _reloadView() {
|
||||
// Force strategy to be re-run by making a copy of the view
|
||||
// Force strategy to be re-run by make a copy of the view
|
||||
const config = this._lovelace!.config;
|
||||
this._lovelace = {
|
||||
...this._lovelace!,
|
||||
config: { ...config, views: config.views.map((view) => ({ ...view })) },
|
||||
config: { ...config, views: [{ ...config.views[0] }] },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -655,13 +565,6 @@ class PanelEnergy extends LitElement {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.centered {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
|
||||
@customElement("energy-overview-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [],
|
||||
dense_section_placement: true,
|
||||
max_columns: 2,
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
// No energy sources available
|
||||
if (
|
||||
!prefs ||
|
||||
(prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0)
|
||||
) {
|
||||
return view;
|
||||
}
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
source.type === "grid" &&
|
||||
(source.flow_from?.length || source.flow_to?.length)
|
||||
) as GridSourceTypeEnergyPreference;
|
||||
const hasReturn = hasGrid && hasGrid.flow_to.length > 0;
|
||||
const hasSolar = prefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
const hasWater = prefs.energy_sources.some(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
const hasPowerSources = prefs.energy_sources.find(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
const hasPowerDevices = prefs.device_consumption.find(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
|
||||
const overviewSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
column_span: 24,
|
||||
cards: [],
|
||||
};
|
||||
if (hasPowerSources && hasPowerDevices) {
|
||||
overviewSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
|
||||
type: "power-sankey",
|
||||
collection_key: collectionKey,
|
||||
grid_options: {
|
||||
columns: 24,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
overviewSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
|
||||
type: "energy-distribution",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) {
|
||||
overviewSection.cards!.push({
|
||||
type: "energy-sources-table",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
view.sections!.push(overviewSection);
|
||||
|
||||
const electricitySection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.electricity"),
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/energy/electricity",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (hasPowerSources) {
|
||||
electricitySection.cards!.push({
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
if (prefs!.device_consumption.length > 3) {
|
||||
electricitySection.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_top_consumers_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: collectionKey,
|
||||
max_devices: 3,
|
||||
modes: ["bar"],
|
||||
});
|
||||
} else if (hasGrid) {
|
||||
const gauges: LovelaceCardConfig[] = [];
|
||||
// Only include if we have a grid source & return.
|
||||
if (hasReturn) {
|
||||
gauges.push({
|
||||
type: "energy-grid-neutrality-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
gauges.push({
|
||||
type: "energy-carbon-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
|
||||
// Only include if we have a solar source.
|
||||
if (hasSolar) {
|
||||
if (hasReturn) {
|
||||
gauges.push({
|
||||
type: "energy-solar-consumed-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
gauges.push({
|
||||
type: "energy-self-sufficiency-gauge",
|
||||
view_layout: { position: "sidebar" },
|
||||
collection_key: collectionKey,
|
||||
});
|
||||
}
|
||||
|
||||
electricitySection.cards!.push({
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
square: false,
|
||||
cards: gauges,
|
||||
});
|
||||
}
|
||||
|
||||
view.sections!.push(electricitySection);
|
||||
|
||||
if (hasGas) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.gas"),
|
||||
},
|
||||
{
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_gas_graph_title"
|
||||
),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWater) {
|
||||
view.sections!.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize("ui.panel.energy.overview.water"),
|
||||
},
|
||||
{
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_water_graph_title"
|
||||
),
|
||||
type: "energy-water-graph",
|
||||
collection_key: collectionKey,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"energy-overview-view-strategy": EnergyViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,57 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type {
|
||||
EnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import { getEnergyPreferences } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
|
||||
@customElement("energy-electricity-view-strategy")
|
||||
export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
const setupWizard = async (): Promise<LovelaceViewConfig> => {
|
||||
await import("../cards/energy-setup-wizard-card");
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "custom:energy-setup-wizard-card",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("energy-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = { cards: [] };
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
let prefs: EnergyPreferences;
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
// No energy sources available
|
||||
if (
|
||||
!prefs ||
|
||||
(prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0)
|
||||
) {
|
||||
try {
|
||||
prefs = await getEnergyPreferences(hass);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return setupWizard();
|
||||
}
|
||||
view.cards!.push({
|
||||
type: "markdown",
|
||||
content: `An error occurred while fetching your energy preferences: ${err.message}.`,
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
// No energy sources available, start from scratch
|
||||
if (
|
||||
prefs!.device_consumption.length === 0 &&
|
||||
prefs!.energy_sources.length === 0
|
||||
) {
|
||||
return setupWizard();
|
||||
}
|
||||
|
||||
view.type = "sidebar";
|
||||
|
||||
const hasGrid = prefs.energy_sources.find(
|
||||
@@ -43,9 +63,13 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
const hasSolar = prefs.energy_sources.some(
|
||||
(source) => source.type === "solar"
|
||||
);
|
||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||
const hasBattery = prefs.energy_sources.some(
|
||||
(source) => source.type === "battery"
|
||||
);
|
||||
const hasWater = prefs.energy_sources.some(
|
||||
(source) => source.type === "water"
|
||||
);
|
||||
|
||||
view.cards!.push({
|
||||
type: "energy-compare",
|
||||
@@ -70,6 +94,24 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a gas source.
|
||||
if (hasGas) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
|
||||
type: "energy-gas-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a water source.
|
||||
if (hasWater) {
|
||||
view.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
|
||||
type: "energy-water-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
// Only include if we have a grid or battery.
|
||||
if (hasGrid || hasBattery) {
|
||||
view.cards!.push({
|
||||
@@ -80,14 +122,13 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGrid || hasSolar || hasBattery) {
|
||||
if (hasGrid || hasSolar || hasGas || hasWater || hasBattery) {
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_sources_table_title"
|
||||
),
|
||||
type: "energy-sources-table",
|
||||
collection_key: "energy_dashboard",
|
||||
types: ["grid", "solar", "battery"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,6 +170,20 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
|
||||
// Only include if we have at least 1 device in the config.
|
||||
if (prefs.device_consumption.length) {
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
const showFloorsNAreas = !prefs.device_consumption.some(
|
||||
(d) => d.included_in_stat
|
||||
);
|
||||
@@ -139,20 +194,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
group_by_floor: showFloorsNAreas,
|
||||
group_by_area: showFloorsNAreas,
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_graph_title"
|
||||
),
|
||||
type: "energy-devices-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
view.cards!.push({
|
||||
title: hass.localize(
|
||||
"ui.panel.energy.cards.energy_devices_detail_graph_title"
|
||||
),
|
||||
type: "energy-devices-detail-graph",
|
||||
collection_key: "energy_dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
@@ -161,6 +202,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"energy-electricity-view-strategy": EnergyElectricityViewStrategy;
|
||||
"energy-view-strategy": EnergyViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
@@ -11,7 +10,12 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
computeAreaTileCardConfig,
|
||||
getAreas,
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
|
||||
export interface LightViewStrategyConfig {
|
||||
type: "light";
|
||||
@@ -81,9 +85,9 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
_config: LightViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
@@ -95,11 +99,10 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
|
||||
const entities = findEntities(allEntities, lightFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
@@ -128,7 +131,7 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
if (home.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
@@ -143,7 +146,7 @@ export class LightViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForLight(hierarchy.areas, hass, entities);
|
||||
const areaCards = processAreasForLight(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { css, LitElement, nothing, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { isNumericFromAttributes } from "../../../common/number/format_number";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import type { LovelaceCardFeature } from "../types";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
BarGaugeCardFeatureConfig,
|
||||
@@ -18,7 +17,7 @@ export const supportsBarGaugeCardFeature = (
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "sensor" && isNumericFromAttributes(stateObj.attributes);
|
||||
return domain === "sensor" && stateObj.attributes.unit_of_measurement === "%";
|
||||
};
|
||||
|
||||
@customElement("hui-bar-gauge-card-feature")
|
||||
@@ -35,11 +34,6 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-bar-gauge-card-feature-editor");
|
||||
return document.createElement("hui-bar-gauge-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: BarGaugeCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
@@ -59,20 +53,8 @@ class HuiBarGaugeCardFeature extends LitElement implements LovelaceCardFeature {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this.context.entity_id];
|
||||
const min = this._config.min ?? 0;
|
||||
const max = this._config.max ?? 100;
|
||||
const value = parseFloat(stateObj.state);
|
||||
|
||||
if (isNaN(value) || min >= max) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const percentage = Math.max(
|
||||
0,
|
||||
Math.min(100, ((value - min) / (max - min)) * 100)
|
||||
);
|
||||
|
||||
return html`<div style="width: ${percentage}%"></div>
|
||||
const value = stateObj.state;
|
||||
return html`<div style="width: ${value}%"></div>
|
||||
<div class="bar-gauge-background"></div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,24 +124,16 @@ class HuiHistoryChartCardFeature
|
||||
}
|
||||
|
||||
const hourToShow = this._config.hours_to_show ?? DEFAULT_HOURS_TO_SHOW;
|
||||
const detail = this._config.detail !== false; // default to true (high detail)
|
||||
|
||||
return subscribeHistoryStatesTimeWindow(
|
||||
this.hass!,
|
||||
(historyStates) => {
|
||||
// sample to 1 point per hour for low detail or 1 point per 5 pixels for high detail
|
||||
const maxDetails = detail
|
||||
? Math.max(10, this.clientWidth / 5, hourToShow)
|
||||
: Math.max(10, hourToShow);
|
||||
const useMean = !detail;
|
||||
const { points, yAxisOrigin } =
|
||||
coordinatesMinimalResponseCompressedState(
|
||||
historyStates[this.context!.entity_id!],
|
||||
this.clientWidth,
|
||||
this.clientHeight,
|
||||
maxDetails,
|
||||
undefined,
|
||||
useMean
|
||||
this.clientWidth / 5 // sample to 1 point per 5 pixels
|
||||
);
|
||||
this._coordinates = points;
|
||||
this._yAxisOrigin = yAxisOrigin;
|
||||
|
||||
@@ -199,7 +199,6 @@ export interface UpdateActionsCardFeatureConfig {
|
||||
export interface TrendGraphCardFeatureConfig {
|
||||
type: "trend-graph";
|
||||
hours_to_show?: number;
|
||||
detail?: boolean;
|
||||
}
|
||||
|
||||
export const AREA_CONTROLS = [
|
||||
@@ -227,8 +226,6 @@ export interface AreaControlsCardFeatureConfig {
|
||||
|
||||
export interface BarGaugeCardFeatureConfig {
|
||||
type: "bar-gauge";
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export type LovelaceCardFeaturePosition = "bottom" | "inline";
|
||||
|
||||
@@ -203,7 +203,7 @@ function formatTooltip(
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: <div style="direction:ltr; display: inline;">${value} ${unit}</div>`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let footer = "";
|
||||
|
||||
@@ -135,13 +135,11 @@ export class HuiEnergyDevicesGraphCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const modes = this._getAllowedModes();
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${modes.length > 1
|
||||
${this._getAllowedModes().length > 1
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this._chartType === "pie"
|
||||
@@ -168,7 +166,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this._chartType,
|
||||
this._legendData
|
||||
)}
|
||||
.height=${`${Math.max(modes.includes("pie") ? 300 : 100, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
|
||||
.extraComponents=${[PieChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@@ -187,7 +185,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
this.hass.locale,
|
||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh`;
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
return `${title}${params.marker} ${params.seriesName}: ${value}`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
@@ -494,7 +492,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
show: true,
|
||||
position: "center",
|
||||
color: computedStyle.getPropertyValue("--secondary-text-color"),
|
||||
fontSize: computedStyle.getPropertyValue("--ha-font-size-m"),
|
||||
fontSize: computedStyle.getPropertyValue("--ha-font-size-l"),
|
||||
lineHeight: 24,
|
||||
fontWeight: "bold",
|
||||
formatter: `{a}\n${formatNumber(totalChart, this.hass.locale)} kWh`,
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
@@ -39,8 +38,6 @@ class HuiEnergySankeyCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: EnergySankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
@@ -388,14 +385,7 @@ class HuiEnergySankeyCard
|
||||
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<ha-card .header=${this._config.title}>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
@@ -412,9 +402,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
@@ -520,18 +508,17 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: calc(
|
||||
var(--row-size, 8) *
|
||||
(var(--row-height, 50px) + var(--row-gap, 0px)) - var(--row-gap, 0px)
|
||||
);
|
||||
}
|
||||
ha-card {
|
||||
height: 400px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,739 +0,0 @@
|
||||
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 { classMap } from "lit/directives/class-map";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import type { PowerSankeyCardConfig } from "../types";
|
||||
import "../../../../components/chart/ha-sankey-chart";
|
||||
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
const DEFAULT_CONFIG: Partial<PowerSankeyCardConfig> = {
|
||||
group_by_floor: true,
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
interface PowerData {
|
||||
solar: number;
|
||||
from_grid: number;
|
||||
to_grid: number;
|
||||
from_battery: number;
|
||||
to_battery: number;
|
||||
grid_to_battery: number;
|
||||
battery_to_grid: number;
|
||||
solar_to_battery: number;
|
||||
solar_to_grid: number;
|
||||
used_solar: number;
|
||||
used_grid: number;
|
||||
used_battery: number;
|
||||
used_total: number;
|
||||
}
|
||||
|
||||
@customElement("hui-power-sankey-card")
|
||||
class HuiPowerSankeyCard
|
||||
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: PowerSankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: PowerSankeyCardConfig): void {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
getEnergyDataCollection(this.hass, {
|
||||
key: this._config?.collection_key,
|
||||
}).subscribe((data) => {
|
||||
this._data = data;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public getCardSize(): Promise<number> | number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 6,
|
||||
min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (
|
||||
changedProps.has("_config") ||
|
||||
changedProps.has("_data") ||
|
||||
changedProps.has("_isMobileSize")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if any of the tracked entity states have changed
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || !this._entities.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only update if one of our tracked entities changed
|
||||
for (const entityId of this._entities) {
|
||||
if (oldHass.states[entityId] !== this.hass.states[entityId]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._data) {
|
||||
return html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.loading"
|
||||
)}`;
|
||||
}
|
||||
|
||||
const prefs = this._data.prefs;
|
||||
const powerData = this._computePowerData(prefs);
|
||||
const computedStyle = getComputedStyle(this);
|
||||
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
|
||||
// Create home node
|
||||
const homeNode: Node = {
|
||||
id: "home",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||
),
|
||||
value: Math.max(0, powerData.used_total),
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
|
||||
// Add battery source and sink if available
|
||||
if (powerData.from_battery > 0) {
|
||||
nodes.push({
|
||||
id: "battery",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: powerData.from_battery,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-battery-out-color")
|
||||
.trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
if (powerData.to_battery > 0) {
|
||||
nodes.push({
|
||||
id: "battery_in",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
||||
),
|
||||
value: powerData.to_battery,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-battery-in-color")
|
||||
.trim(),
|
||||
index: 1,
|
||||
});
|
||||
if (powerData.grid_to_battery > 0) {
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "battery_in",
|
||||
});
|
||||
}
|
||||
if (powerData.solar_to_battery > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "battery_in",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add grid source if available
|
||||
if (powerData.from_grid > 0) {
|
||||
nodes.push({
|
||||
id: "grid",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: powerData.from_grid,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-grid-consumption-color")
|
||||
.trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "grid",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
// Add solar if available
|
||||
if (powerData.solar > 0) {
|
||||
nodes.push({
|
||||
id: "solar",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.solar"
|
||||
),
|
||||
value: powerData.solar,
|
||||
color: computedStyle.getPropertyValue("--energy-solar-color").trim(),
|
||||
index: 0,
|
||||
});
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "home",
|
||||
});
|
||||
}
|
||||
|
||||
// Add grid return if available
|
||||
if (powerData.to_grid > 0) {
|
||||
nodes.push({
|
||||
id: "grid_return",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.grid"
|
||||
),
|
||||
value: powerData.to_grid,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--energy-grid-return-color")
|
||||
.trim(),
|
||||
index: 2,
|
||||
});
|
||||
if (powerData.battery_to_grid > 0) {
|
||||
links.push({
|
||||
source: "battery",
|
||||
target: "grid_return",
|
||||
});
|
||||
}
|
||||
if (powerData.solar_to_grid > 0) {
|
||||
links.push({
|
||||
source: "solar",
|
||||
target: "grid_return",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let untrackedConsumption = homeNode.value;
|
||||
const deviceNodes: Node[] = [];
|
||||
const parentLinks: Record<string, string> = {};
|
||||
prefs.device_consumption.forEach((device, idx) => {
|
||||
if (!device.stat_rate) {
|
||||
return;
|
||||
}
|
||||
const value = this._getCurrentPower(device.stat_rate);
|
||||
|
||||
if (value < 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: device.stat_rate,
|
||||
label: device.name || this._getEntityLabel(device.stat_rate),
|
||||
value,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: device.included_in_stat,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({
|
||||
source: node.parent,
|
||||
target: node.id,
|
||||
});
|
||||
} else {
|
||||
untrackedConsumption -= value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
});
|
||||
const devicesWithoutParent = deviceNodes.filter(
|
||||
(node) => !parentLinks[node.id]
|
||||
);
|
||||
|
||||
const { group_by_area, group_by_floor } = this._config;
|
||||
if (group_by_area || group_by_floor) {
|
||||
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
|
||||
|
||||
Object.keys(floors)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(this.hass.floors[b]?.level ?? -Infinity) -
|
||||
(this.hass.floors[a]?.level ?? -Infinity)
|
||||
)
|
||||
.forEach((floorId) => {
|
||||
let floorNodeId = `floor_${floorId}`;
|
||||
if (floorId === "no_floor" || !group_by_floor) {
|
||||
// link "no_floor" areas to home
|
||||
floorNodeId = "home";
|
||||
} else {
|
||||
nodes.push({
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
index: 2,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: floorNodeId,
|
||||
});
|
||||
}
|
||||
floors[floorId].areas.forEach((areaId) => {
|
||||
let targetNodeId: string;
|
||||
|
||||
if (areaId === "no_area" || !group_by_area) {
|
||||
// If group_by_area is false, link devices to floor or home
|
||||
targetNodeId = floorNodeId;
|
||||
} else {
|
||||
// Create area node and link it to floor
|
||||
const areaNodeId = `area_${areaId}`;
|
||||
nodes.push({
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]?.name || areaId,
|
||||
value: areas[areaId].value,
|
||||
index: 3,
|
||||
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||
});
|
||||
links.push({
|
||||
source: floorNodeId,
|
||||
target: areaNodeId,
|
||||
value: areas[areaId].value,
|
||||
});
|
||||
targetNodeId = areaNodeId;
|
||||
}
|
||||
|
||||
// Link devices to the appropriate target (area, floor, or home)
|
||||
areas[areaId].devices.forEach((device) => {
|
||||
links.push({
|
||||
source: targetNodeId,
|
||||
target: device.id,
|
||||
value: device.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devicesWithoutParent.forEach((deviceNode) => {
|
||||
links.push({
|
||||
source: "home",
|
||||
target: deviceNode.id,
|
||||
value: deviceNode.value,
|
||||
});
|
||||
});
|
||||
}
|
||||
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
|
||||
deviceSections.forEach((section, index) => {
|
||||
section.forEach((node: Node) => {
|
||||
nodes.push({ ...node, index: 4 + index });
|
||||
});
|
||||
});
|
||||
|
||||
// untracked consumption
|
||||
if (untrackedConsumption > 0) {
|
||||
nodes.push({
|
||||
id: "untracked",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||
),
|
||||
value: untrackedConsumption,
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 3 + deviceSections.length,
|
||||
});
|
||||
links.push({
|
||||
source: "home",
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
const vertical =
|
||||
this._config.layout === "vertical" ||
|
||||
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
class=${classMap({
|
||||
"is-grid": this.layout === "grid",
|
||||
"is-panel": this.layout === "panel",
|
||||
"is-vertical": vertical,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${hasData
|
||||
? html`<ha-sankey-chart
|
||||
.data=${{ nodes, links }}
|
||||
.vertical=${vertical}
|
||||
.valueFormatter=${this._valueFormatter}
|
||||
></ha-sankey-chart>`
|
||||
: html`${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.no_data"
|
||||
)}`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kW</div>`;
|
||||
|
||||
/**
|
||||
* Compute real-time power data from current entity states.
|
||||
* Similar to computeConsumptionData but for instantaneous power.
|
||||
*/
|
||||
private _computePowerData(prefs: EnergyPreferences): PowerData {
|
||||
// Clear tracked entities and rebuild the set
|
||||
this._entities.clear();
|
||||
|
||||
let solar = 0;
|
||||
let from_grid = 0;
|
||||
let to_grid = 0;
|
||||
let from_battery = 0;
|
||||
let to_battery = 0;
|
||||
|
||||
// Collect solar power
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "solar")
|
||||
.forEach((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
solar += value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collect grid power (positive = import, negative = export)
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "grid" && source.power)
|
||||
.forEach((source) => {
|
||||
if (source.type === "grid" && source.power) {
|
||||
source.power.forEach((powerSource) => {
|
||||
const value = this._getCurrentPower(powerSource.stat_rate);
|
||||
if (value > 0) {
|
||||
from_grid += value;
|
||||
} else if (value < 0) {
|
||||
to_grid += Math.abs(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Collect battery power (positive = discharge, negative = charge)
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "battery")
|
||||
.forEach((source) => {
|
||||
if (source.type === "battery" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
from_battery += value;
|
||||
} else if (value < 0) {
|
||||
to_battery += Math.abs(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate total consumption
|
||||
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
|
||||
|
||||
// Determine power routing using priority logic
|
||||
// Priority: Solar -> Battery_In, Solar -> Grid_Out, Battery_Out -> Grid_Out,
|
||||
// Grid_In -> Battery_In, Solar -> Consumption, Battery_Out -> Consumption, Grid_In -> Consumption
|
||||
|
||||
let solar_remaining = solar;
|
||||
let grid_remaining = from_grid;
|
||||
let battery_remaining = from_battery;
|
||||
let to_battery_remaining = to_battery;
|
||||
let to_grid_remaining = to_grid;
|
||||
let used_total_remaining = Math.max(used_total, 0);
|
||||
|
||||
let grid_to_battery = 0;
|
||||
let battery_to_grid = 0;
|
||||
let solar_to_battery = 0;
|
||||
let solar_to_grid = 0;
|
||||
let used_solar = 0;
|
||||
let used_battery = 0;
|
||||
let used_grid = 0;
|
||||
|
||||
// Handle excess grid input to battery first
|
||||
const excess_grid_in_after_consumption = Math.max(
|
||||
0,
|
||||
Math.min(to_battery_remaining, grid_remaining - used_total_remaining)
|
||||
);
|
||||
grid_to_battery += excess_grid_in_after_consumption;
|
||||
to_battery_remaining -= excess_grid_in_after_consumption;
|
||||
grid_remaining -= excess_grid_in_after_consumption;
|
||||
|
||||
// Solar -> Battery_In
|
||||
solar_to_battery = Math.min(solar_remaining, to_battery_remaining);
|
||||
to_battery_remaining -= solar_to_battery;
|
||||
solar_remaining -= solar_to_battery;
|
||||
|
||||
// Solar -> Grid_Out
|
||||
solar_to_grid = Math.min(solar_remaining, to_grid_remaining);
|
||||
to_grid_remaining -= solar_to_grid;
|
||||
solar_remaining -= solar_to_grid;
|
||||
|
||||
// Battery_Out -> Grid_Out
|
||||
battery_to_grid = Math.min(battery_remaining, to_grid_remaining);
|
||||
battery_remaining -= battery_to_grid;
|
||||
to_grid_remaining -= battery_to_grid;
|
||||
|
||||
// Grid_In -> Battery_In (second pass)
|
||||
const grid_to_battery_2 = Math.min(grid_remaining, to_battery_remaining);
|
||||
grid_to_battery += grid_to_battery_2;
|
||||
grid_remaining -= grid_to_battery_2;
|
||||
to_battery_remaining -= grid_to_battery_2;
|
||||
|
||||
// Solar -> Consumption
|
||||
used_solar = Math.min(used_total_remaining, solar_remaining);
|
||||
used_total_remaining -= used_solar;
|
||||
solar_remaining -= used_solar;
|
||||
|
||||
// Battery_Out -> Consumption
|
||||
used_battery = Math.min(battery_remaining, used_total_remaining);
|
||||
battery_remaining -= used_battery;
|
||||
used_total_remaining -= used_battery;
|
||||
|
||||
// Grid_In -> Consumption
|
||||
used_grid = Math.min(used_total_remaining, grid_remaining);
|
||||
grid_remaining -= used_grid;
|
||||
used_total_remaining -= used_grid;
|
||||
|
||||
return {
|
||||
solar,
|
||||
from_grid,
|
||||
to_grid,
|
||||
from_battery,
|
||||
to_battery,
|
||||
grid_to_battery,
|
||||
battery_to_grid,
|
||||
solar_to_battery,
|
||||
solar_to_grid,
|
||||
used_solar,
|
||||
used_grid,
|
||||
used_battery,
|
||||
used_total: Math.max(0, used_total),
|
||||
};
|
||||
}
|
||||
|
||||
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||
no_area: {
|
||||
value: 0,
|
||||
devices: [],
|
||||
},
|
||||
};
|
||||
const floors: Record<string, { value: number; areas: string[] }> = {
|
||||
no_floor: {
|
||||
value: 0,
|
||||
areas: ["no_area"],
|
||||
},
|
||||
};
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const entity = this.hass.states[deviceNode.id];
|
||||
const { area, floor } = entity
|
||||
? getEntityContext(
|
||||
entity,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: { area: null, floor: null };
|
||||
if (area) {
|
||||
if (area.area_id in areas) {
|
||||
areas[area.area_id].value += deviceNode.value;
|
||||
areas[area.area_id].devices.push(deviceNode);
|
||||
} else {
|
||||
areas[area.area_id] = {
|
||||
value: deviceNode.value,
|
||||
devices: [deviceNode],
|
||||
};
|
||||
}
|
||||
// see if the area has a floor
|
||||
if (floor) {
|
||||
if (floor.floor_id in floors) {
|
||||
floors[floor.floor_id].value += deviceNode.value;
|
||||
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
|
||||
floors[floor.floor_id].areas.push(area.area_id);
|
||||
}
|
||||
} else {
|
||||
floors[floor.floor_id] = {
|
||||
value: deviceNode.value,
|
||||
areas: [area.area_id],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
floors.no_floor.value += deviceNode.value;
|
||||
if (!floors.no_floor.areas.includes(area.area_id)) {
|
||||
floors.no_floor.areas.unshift(area.area_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
areas.no_area.value += deviceNode.value;
|
||||
areas.no_area.devices.push(deviceNode);
|
||||
}
|
||||
});
|
||||
return { areas, floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organizes device nodes into hierarchical sections based on parent-child relationships.
|
||||
*/
|
||||
protected _getDeviceSections(
|
||||
parentLinks: Record<string, string>,
|
||||
deviceNodes: Node[]
|
||||
): Node[][] {
|
||||
const parentSection: Node[] = [];
|
||||
const childSection: Node[] = [];
|
||||
const parentIds = Object.values(parentLinks);
|
||||
const remainingLinks: typeof parentLinks = {};
|
||||
|
||||
deviceNodes.forEach((deviceNode) => {
|
||||
const isChild = deviceNode.id in parentLinks;
|
||||
const isParent = parentIds.includes(deviceNode.id);
|
||||
if (isParent && !isChild) {
|
||||
// Top-level parents (have children but no parents themselves)
|
||||
parentSection.push(deviceNode);
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out links where parent is already in current parent section
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
// Recursively process child section with remaining links
|
||||
return [
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
// Base case: no more parent-child relationships to process
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to kW
|
||||
* @param entityId - The entity ID to get power value from
|
||||
* @returns Power value in kW, or 0 if entity not found or invalid
|
||||
*/
|
||||
private _getCurrentPower(entityId: string): number {
|
||||
// Track this entity for state change detection
|
||||
this._entities.add(entityId);
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return 0;
|
||||
}
|
||||
const value = parseFloat(stateObj.state);
|
||||
if (isNaN(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Normalize to kW based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value / 1000;
|
||||
case "mW":
|
||||
return value / 1000000;
|
||||
case "MW":
|
||||
return value * 1000;
|
||||
case "GW":
|
||||
return value * 1000000;
|
||||
case "TW":
|
||||
return value * 1000000000;
|
||||
default:
|
||||
// Assume kW if no unit or unit is kW
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity label (friendly name or entity ID)
|
||||
* @param entityId - The entity ID to get label for
|
||||
* @returns Friendly name if available, otherwise the entity ID
|
||||
*/
|
||||
private _getEntityLabel(entityId: string): string {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
return stateObj.attributes.friendly_name || entityId;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--chart-max-height: none;
|
||||
}
|
||||
ha-card.is-vertical {
|
||||
height: 500px;
|
||||
}
|
||||
ha-card.is-grid,
|
||||
ha-card.is-panel {
|
||||
height: 100%;
|
||||
}
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-power-sankey-card": HuiPowerSankeyCard;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
|
||||
|
||||
@customElement("hui-power-sources-graph-card")
|
||||
export class HuiPowerSourcesGraphCard
|
||||
@@ -34,8 +33,6 @@ export class HuiPowerSourcesGraphCard
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@state() private _legendData?: CustomLegendOption["data"];
|
||||
|
||||
@state() private _start = startOfToday();
|
||||
|
||||
@state() private _end = endOfToday();
|
||||
@@ -94,8 +91,7 @@ export class HuiPowerSourcesGraphCard
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this._compareStart,
|
||||
this._compareEnd,
|
||||
this._legendData
|
||||
this._compareEnd
|
||||
)}
|
||||
></ha-chart-base>
|
||||
${!this._chartData.some((dataset) => dataset.data!.length)
|
||||
@@ -119,10 +115,9 @@ export class HuiPowerSourcesGraphCard
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
compareStart?: Date,
|
||||
compareEnd?: Date,
|
||||
legendData?: CustomLegendOption["data"]
|
||||
): ECOption => ({
|
||||
...getCommonOptions(
|
||||
compareEnd?: Date
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
locale,
|
||||
@@ -130,18 +125,11 @@ export class HuiPowerSourcesGraphCard
|
||||
"kW",
|
||||
compareStart,
|
||||
compareEnd
|
||||
),
|
||||
legend: {
|
||||
show: true,
|
||||
type: "custom",
|
||||
data: legendData,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
private async _getStatistics(energyData: EnergyData): Promise<void> {
|
||||
const datasets: LineSeriesOption[] = [];
|
||||
this._legendData = [];
|
||||
|
||||
const statIds = {
|
||||
solar: {
|
||||
@@ -250,15 +238,6 @@ export class HuiPowerSourcesGraphCard
|
||||
z: 4 - keyIndex, // draw in reverse order but above positive series
|
||||
});
|
||||
}
|
||||
this._legendData!.push({
|
||||
id: key,
|
||||
secondaryIds: key !== "solar" ? [`${key}-negative`] : [],
|
||||
name: statIds[key].name,
|
||||
itemStyle: {
|
||||
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
|
||||
borderColor: colorHex,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,23 +268,11 @@ export class HuiPowerSourcesGraphCard
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||
),
|
||||
color: computedStyles.getPropertyValue("--primary-text-color"),
|
||||
lineStyle: {
|
||||
type: [7, 2],
|
||||
width: 1.5,
|
||||
},
|
||||
color: computedStyles.getPropertyValue("--primary-color"),
|
||||
lineStyle: { width: 2 },
|
||||
data: usageData,
|
||||
z: 5,
|
||||
});
|
||||
this._legendData!.push({
|
||||
id: "usage",
|
||||
name: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.power_graph.usage"
|
||||
),
|
||||
itemStyle: {
|
||||
color: computedStyles.getPropertyValue("--primary-text-color"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _processData(stats: StatisticValue[][]) {
|
||||
|
||||
@@ -150,6 +150,11 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-summary";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-distribution";
|
||||
title?: string;
|
||||
@@ -231,14 +236,6 @@ export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface PowerSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sankey";
|
||||
title?: string;
|
||||
layout?: "vertical" | "horizontal" | "auto";
|
||||
group_by_floor?: boolean;
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
type: "entity-filter";
|
||||
entities: (EntityFilterEntityConfig | string)[];
|
||||
|
||||
@@ -68,7 +68,6 @@ const LAZY_LOAD_TYPES = {
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"power-sankey": () => import("../cards/energy/hui-power-sankey-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
BarGaugeCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-bar-gauge-card-feature-editor")
|
||||
export class HuiBarGaugeCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: BarGaugeCardFeatureConfig;
|
||||
|
||||
public setConfig(config: BarGaugeCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "min",
|
||||
default: 0,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
default: 100,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema();
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.bar-gauge.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-bar-gauge-card-feature-editor": HuiBarGaugeCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,6 @@ type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
|
||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"bar-gauge",
|
||||
"button",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
|
||||
@@ -20,10 +20,6 @@ const SCHEMA = [
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "detail",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
@customElement("hui-trend-graph-card-feature-editor")
|
||||
@@ -52,10 +48,6 @@ export class HuiTrendGraphCardFeatureEditor
|
||||
data.hours_to_show = DEFAULT_HOURS_TO_SHOW;
|
||||
}
|
||||
|
||||
if (this._config.detail === undefined) {
|
||||
data.detail = true;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
@@ -77,10 +69,6 @@ export class HuiTrendGraphCardFeatureEditor
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
case "detail":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.features.types.trend-graph.detail"
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3,21 +3,20 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
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-md-select";
|
||||
import "../../../../components/ha-md-select-option";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectDashboardDialogParams } from "./show-select-dashboard-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
|
||||
@customElement("hui-dialog-select-dashboard")
|
||||
export class HuiDialogSelectDashboard extends LitElement {
|
||||
@@ -144,9 +143,7 @@ export class HuiDialogSelectDashboard extends LitElement {
|
||||
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
||||
];
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
const currentPath = this._fromUrlPath || defaultPanel;
|
||||
const currentPath = this._fromUrlPath || this.hass.defaultPanel;
|
||||
for (const dashboard of this._dashboards!) {
|
||||
if (dashboard.url_path !== currentPath) {
|
||||
this._toUrlPath = dashboard.url_path;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { fetchConfig } from "../../../../data/lovelace/config/types";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../../../data/lovelace/dashboard";
|
||||
import { getDefaultPanelUrlPath } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { SelectViewDialogParams } from "./show-select-view-dialog";
|
||||
@@ -61,9 +60,6 @@ export class HuiDialogSelectView extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const defaultPanel = getDefaultPanelUrlPath(this.hass);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@@ -80,7 +76,7 @@ export class HuiDialogSelectView extends LitElement {
|
||||
"ui.panel.lovelace.editor.select_view.dashboard_label"
|
||||
)}
|
||||
.disabled=${!this._dashboards.length}
|
||||
.value=${this._urlPath || defaultPanel}
|
||||
.value=${this._urlPath || this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
|
||||
@@ -38,10 +38,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
view: {
|
||||
"original-states": () =>
|
||||
import("./original-states/original-states-view-strategy"),
|
||||
"energy-overview": () =>
|
||||
import("../../energy/strategies/energy-overview-view-strategy"),
|
||||
"energy-electricity": () =>
|
||||
import("../../energy/strategies/energy-electricity-view-strategy"),
|
||||
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
||||
map: () => import("./map/map-view-strategy"),
|
||||
iframe: () => import("./iframe/iframe-view-strategy"),
|
||||
area: () => import("./areas/area-view-strategy"),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||
|
||||
interface HomeStructure {
|
||||
floors: {
|
||||
id: string;
|
||||
areas: string[];
|
||||
}[];
|
||||
areas: string[];
|
||||
}
|
||||
|
||||
export const getHomeStructure = (
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[]
|
||||
): HomeStructure => {
|
||||
const floorAreas = new Map<string, string[]>();
|
||||
const unassignedAreas: string[] = [];
|
||||
|
||||
for (const area of areas) {
|
||||
if (area.floor_id) {
|
||||
if (!floorAreas.has(area.floor_id)) {
|
||||
floorAreas.set(area.floor_id, []);
|
||||
}
|
||||
floorAreas.get(area.floor_id)!.push(area.area_id);
|
||||
} else {
|
||||
unassignedAreas.push(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
const homeStructure: HomeStructure = {
|
||||
floors: floors.map((floor) => ({
|
||||
id: floor.floor_id,
|
||||
areas: floorAreas.get(floor.floor_id) || [],
|
||||
})),
|
||||
areas: unassignedAreas,
|
||||
};
|
||||
|
||||
return homeStructure;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAreas } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import {
|
||||
getSummaryLabel,
|
||||
@@ -45,7 +46,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
};
|
||||
}
|
||||
|
||||
const areas = Object.values(hass.areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
|
||||
const areaViews = areas.map<LovelaceViewRawConfig>((area) => {
|
||||
const path = `areas-${area.area_id}`;
|
||||
|
||||
@@ -22,8 +22,9 @@ import type {
|
||||
MarkdownCardConfig,
|
||||
WeatherForecastCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeMainViewStrategyConfig {
|
||||
@@ -63,10 +64,10 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
config: HomeMainViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
|
||||
const home = getAreasFloorHierarchy(floors, areas);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MediaControlCardConfig } from "../../cards/types";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "./helpers/home-structure";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeMediaPlayersViewStrategyConfig {
|
||||
@@ -84,9 +85,9 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
||||
_config: HomeMediaPlayersViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const home = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@media (max-width: 600px) {
|
||||
:host {
|
||||
--column-gap: var(--ha-view-sections-narrow-column-gap, var(--row-gap));
|
||||
--column-gap: var(--row-gap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
import { setDefaultPanel } from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
|
||||
const USE_SYSTEM_VALUE = "___use_system___";
|
||||
|
||||
@customElement("ha-pick-dashboard-row")
|
||||
class HaPickDashboardRow extends LitElement {
|
||||
@@ -25,7 +23,6 @@ class HaPickDashboardRow extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const value = this.hass.userData?.defaultPanel || USE_SYSTEM_VALUE;
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
@@ -40,18 +37,14 @@ class HaPickDashboardRow extends LitElement {
|
||||
"ui.panel.profile.dashboard.dropdown_label"
|
||||
)}
|
||||
.disabled=${!this._dashboards?.length}
|
||||
.value=${value}
|
||||
.value=${this.hass.defaultPanel}
|
||||
@selected=${this._dashboardChanged}
|
||||
naturalMenuWidth
|
||||
>
|
||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="lovelace">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="home">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.dashboard.default_dashboard_label"
|
||||
)}
|
||||
</ha-list-item>
|
||||
${this._dashboards.map((dashboard) => {
|
||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||
@@ -79,18 +72,11 @@ class HaPickDashboardRow extends LitElement {
|
||||
}
|
||||
|
||||
private _dashboardChanged(ev) {
|
||||
const value = ev.target.value as string;
|
||||
if (!value) {
|
||||
const urlPath = ev.target.value;
|
||||
if (!urlPath || urlPath === this.hass.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
const urlPath = value === USE_SYSTEM_VALUE ? undefined : value;
|
||||
if (urlPath === this.hass.userData?.defaultPanel) {
|
||||
return;
|
||||
}
|
||||
saveFrontendUserData(this.hass.connection, "core", {
|
||||
...this.hass.userData,
|
||||
defaultPanel: urlPath,
|
||||
});
|
||||
setDefaultPanel(this, urlPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,10 +154,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-first-weekday-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
@@ -212,6 +208,10 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-theme-row>
|
||||
<ha-pick-dashboard-row
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
></ha-pick-dashboard-row>
|
||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||
? html`
|
||||
<ha-force-narrow-row
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
@@ -11,7 +10,12 @@ import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
computeAreaTileCardConfig,
|
||||
getAreas,
|
||||
getFloors,
|
||||
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
|
||||
|
||||
export interface SecurityViewStrategyConfig {
|
||||
type: "security";
|
||||
@@ -123,9 +127,9 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
_config: SecurityViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
const areas = getAreas(hass.areas);
|
||||
const floors = getFloors(hass.floors);
|
||||
const home = getHomeStructure(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
@@ -137,11 +141,10 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
|
||||
const entities = findEntities(allEntities, securityFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
for (const floorStructure of home.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
@@ -170,7 +173,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
if (home.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
@@ -185,11 +188,7 @@ export class SecurityViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForSecurity(
|
||||
hierarchy.areas,
|
||||
hass,
|
||||
entities
|
||||
);
|
||||
const areaCards = processAreasForSecurity(home.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
|
||||
@@ -8,16 +8,13 @@ import {
|
||||
subscribeServices,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { promiseTimeout } from "../common/util/promise-timeout";
|
||||
import { subscribeAreaRegistry } from "../data/area_registry";
|
||||
import { broadcastConnectionStatus } from "../data/connection-status";
|
||||
import { subscribeDeviceRegistry } from "../data/device_registry";
|
||||
import {
|
||||
subscribeFrontendSystemData,
|
||||
subscribeFrontendUserData,
|
||||
} from "../data/frontend";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import { DEFAULT_PANEL } from "../data/panel";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -36,6 +33,7 @@ import { fetchWithAuth } from "../util/fetch-with-auth";
|
||||
import { getState } from "../util/ha-pref-storage";
|
||||
import hassCallApi, { hassCallApiRaw } from "../util/hass-call-api";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
|
||||
export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
@@ -61,9 +59,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
panels: null as any,
|
||||
services: null as any,
|
||||
user: null as any,
|
||||
userData: undefined,
|
||||
systemData: undefined,
|
||||
panelUrl: (this as any)._panelUrl,
|
||||
defaultPanel: DEFAULT_PANEL,
|
||||
language,
|
||||
selectedLanguage: null,
|
||||
locale: {
|
||||
@@ -76,6 +73,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
},
|
||||
resources: null as any,
|
||||
localize: () => "",
|
||||
|
||||
translationMetadata,
|
||||
dockedSidebar: "docked",
|
||||
vibrate: true,
|
||||
@@ -211,9 +209,9 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value != null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
formatEntityName: (stateObj) => computeStateName(stateObj),
|
||||
...getState(),
|
||||
...this._pendingHass,
|
||||
formatEntityName: (stateObj) => computeStateName(stateObj),
|
||||
};
|
||||
|
||||
this.hassConnected();
|
||||
@@ -284,26 +282,10 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
subscribeConfig(conn, (config) => this._updateHass({ config }));
|
||||
subscribeServices(conn, (services) => this._updateHass({ services }));
|
||||
subscribePanels(conn, (panels) => this._updateHass({ panels }));
|
||||
// Catch errors to userData and systemData subscription (e.g. if the
|
||||
// backend isn't up to date) and set to null so frontend can continue
|
||||
subscribeFrontendUserData(conn, "core", ({ value: userData }) =>
|
||||
this._updateHass({ userData: userData || {} })
|
||||
).catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to subscribe to user data, setting to empty object"
|
||||
);
|
||||
this._updateHass({ userData: {} });
|
||||
});
|
||||
subscribeFrontendSystemData(conn, "core", ({ value: systemData }) =>
|
||||
this._updateHass({ systemData: systemData || {} })
|
||||
).catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Failed to subscribe to system data, setting to empty object"
|
||||
);
|
||||
this._updateHass({ systemData: {} });
|
||||
subscribeFrontendUserData(conn, "core", ({ value: userData }) => {
|
||||
this._updateHass({ userData });
|
||||
});
|
||||
|
||||
clearInterval(this.__backendPingInterval);
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
@@ -11,9 +12,9 @@ import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { HassElement } from "./hass-element";
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { Redirects } from "../panels/my/ha-panel-my";
|
||||
|
||||
@@ -61,22 +62,21 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
|
||||
private _registerShortcut() {
|
||||
const shortcutManager = new ShortcutManager();
|
||||
shortcutManager.add({
|
||||
tinykeys(window, {
|
||||
// Those are for latin keyboards that have e, c, m keys
|
||||
e: { handler: (ev) => this._showQuickBar(ev) },
|
||||
c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
|
||||
m: { handler: (ev) => this._createMyLink(ev) },
|
||||
a: { handler: (ev) => this._showVoiceCommandDialog(ev) },
|
||||
d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
|
||||
e: (ev) => this._showQuickBar(ev),
|
||||
c: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
|
||||
m: (ev) => this._createMyLink(ev),
|
||||
a: (ev) => this._showVoiceCommandDialog(ev),
|
||||
d: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
|
||||
// Workaround see https://github.com/jamiebuilds/tinykeys/issues/130
|
||||
"Shift+?": { handler: (ev) => this._showShortcutDialog(ev) },
|
||||
"Shift+?": (ev) => this._showShortcutDialog(ev),
|
||||
// Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts)
|
||||
KeyE: { handler: (ev) => this._showQuickBar(ev) },
|
||||
KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
|
||||
KeyM: { handler: (ev) => this._createMyLink(ev) },
|
||||
KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) },
|
||||
KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
|
||||
KeyE: (ev) => this._showQuickBar(ev),
|
||||
KeyC: (ev) => this._showQuickBar(ev, QuickBarMode.Command),
|
||||
KeyM: (ev) => this._createMyLink(ev),
|
||||
KeyA: (ev) => this._showVoiceCommandDialog(ev),
|
||||
KeyD: (ev) => this._showQuickBar(ev, QuickBarMode.Device),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
private _showVoiceCommandDialog(e: KeyboardEvent) {
|
||||
if (
|
||||
!this.hass?.enableShortcuts ||
|
||||
!canOverrideAlphanumericInput(e.composedPath()) ||
|
||||
!this._conversation(this.hass.config.components)
|
||||
) {
|
||||
return;
|
||||
@@ -104,7 +105,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
e: KeyboardEvent,
|
||||
mode: QuickBarMode = QuickBarMode.Entity
|
||||
) {
|
||||
if (!this._canShowQuickBar()) {
|
||||
if (!this._canShowQuickBar(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
|
||||
private _showShortcutDialog(e: KeyboardEvent) {
|
||||
if (!this._canShowQuickBar()) {
|
||||
if (!this._canShowQuickBar(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,7 +131,10 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
|
||||
private async _createMyLink(e: KeyboardEvent) {
|
||||
if (!this.hass?.enableShortcuts) {
|
||||
if (
|
||||
!this.hass?.enableShortcuts ||
|
||||
!canOverrideAlphanumericInput(e.composedPath())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,7 +193,11 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
});
|
||||
}
|
||||
|
||||
private _canShowQuickBar() {
|
||||
return this.hass?.user?.is_admin && this.hass.enableShortcuts;
|
||||
private _canShowQuickBar(e: KeyboardEvent) {
|
||||
return (
|
||||
this.hass?.user?.is_admin &&
|
||||
this.hass.enableShortcuts &&
|
||||
canOverrideAlphanumericInput(e.composedPath())
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,14 +7,20 @@ interface DockSidebarParams {
|
||||
dock: HomeAssistant["dockedSidebar"];
|
||||
}
|
||||
|
||||
interface DefaultPanelParams {
|
||||
defaultPanel: HomeAssistant["defaultPanel"];
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"hass-dock-sidebar": DockSidebarParams;
|
||||
"hass-default-panel": DefaultPanelParams;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
|
||||
"hass-default-panel": HASSDomEvent<DefaultPanelParams>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +32,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||
storeState(this.hass!);
|
||||
});
|
||||
this.addEventListener("hass-default-panel", (ev) => {
|
||||
this._updateHass({ defaultPanel: ev.detail.defaultPanel });
|
||||
storeState(this.hass!);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1237,8 +1237,7 @@
|
||||
"times": "times"
|
||||
},
|
||||
"summary": "Summary",
|
||||
"description": "Description",
|
||||
"location": "Location"
|
||||
"description": "Description"
|
||||
},
|
||||
"views": {
|
||||
"dayGridMonth": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridMonth%]",
|
||||
@@ -2457,10 +2456,7 @@
|
||||
"delete_floor": "Delete floor",
|
||||
"confirm_delete": "Delete floor?",
|
||||
"confirm_delete_text": "Deleting the floor will unassign all areas from it."
|
||||
},
|
||||
"area_reorder_failed": "Failed to reorder areas",
|
||||
"area_move_failed": "Failed to move area",
|
||||
"floor_reorder_failed": "Failed to reorder floors"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"create_area": "Create area",
|
||||
@@ -3519,12 +3515,8 @@
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"create": "Create",
|
||||
"set_default": "Set as default",
|
||||
"remove_default": "Remove as default",
|
||||
"set_default_confirm_title": "Set as default dashboard?",
|
||||
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
|
||||
"remove_default_confirm_title": "Remove default dashboard?",
|
||||
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
|
||||
"set_default": "Set as default on this device",
|
||||
"remove_default": "Remove as default on this device"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -6768,6 +6760,7 @@
|
||||
},
|
||||
"analytics": {
|
||||
"caption": "Analytics",
|
||||
"header": "Home Assistant analytics",
|
||||
"description": "Learn how to share data to improve Home Assistant",
|
||||
"preferences": {
|
||||
"base": {
|
||||
@@ -6785,10 +6778,17 @@
|
||||
"diagnostics": {
|
||||
"title": "Diagnostics",
|
||||
"description": "Share crash reports when unexpected errors occur."
|
||||
},
|
||||
"snapshots": {
|
||||
"title": "Devices",
|
||||
"description": "Generic information about your devices.",
|
||||
"header": "Device analytics",
|
||||
"info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
|
||||
"learn_more": "Learn more about the device database and how we process your data"
|
||||
}
|
||||
},
|
||||
"need_base_enabled": "You need to enable basic analytics for this option to be available",
|
||||
"learn_more": "How we process your data",
|
||||
"learn_more": "Learn how we process your data",
|
||||
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
||||
"download_device_info": "Preview device analytics"
|
||||
},
|
||||
@@ -7174,7 +7174,7 @@
|
||||
"grid": "Grid",
|
||||
"solar": "Solar",
|
||||
"battery": "Battery",
|
||||
"usage": "Consumption"
|
||||
"usage": "Used"
|
||||
},
|
||||
"energy_compare": {
|
||||
"info": "You are comparing the period {start} with the period {end}",
|
||||
@@ -8432,13 +8432,10 @@
|
||||
"no_compatible_controls": "No compatible controls available for this area"
|
||||
},
|
||||
"bar-gauge": {
|
||||
"label": "Bar gauge",
|
||||
"min": "Minimum value",
|
||||
"max": "Maximum value"
|
||||
"label": "Bar gauge"
|
||||
},
|
||||
"trend-graph": {
|
||||
"label": "Trend graph",
|
||||
"detail": "Show more detail"
|
||||
"label": "Trend graph"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8674,11 +8671,9 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"header": "Dashboard",
|
||||
"description": "Pick a default dashboard to show.",
|
||||
"description": "Pick a default dashboard for this device.",
|
||||
"dropdown_label": "Dashboard",
|
||||
"lovelace": "Overview",
|
||||
"home": "Home",
|
||||
"system": "Auto (use system settings)"
|
||||
"default_dashboard_label": "Overview (default)"
|
||||
},
|
||||
"change_password": {
|
||||
"header": "Change password",
|
||||
@@ -9439,11 +9434,6 @@
|
||||
}
|
||||
},
|
||||
"energy": {
|
||||
"overview": {
|
||||
"electricity": "Electricity",
|
||||
"gas": "Gas",
|
||||
"water": "Water"
|
||||
},
|
||||
"download_data": "[%key:ui::panel::history::download_data%]",
|
||||
"configure": "[%key:ui::dialogs::quick-bar::commands::navigation::energy%]",
|
||||
"setup": {
|
||||
@@ -9468,9 +9458,7 @@
|
||||
"energy_sources_table_title": "Sources",
|
||||
"energy_devices_graph_title": "Individual devices total usage",
|
||||
"energy_devices_detail_graph_title": "Individual devices detail usage",
|
||||
"energy_sankey_title": "Energy flow",
|
||||
"energy_top_consumers_title": "Top consumers",
|
||||
"power_sankey_title": "Current power flow"
|
||||
"energy_sankey_title": "Energy flow"
|
||||
}
|
||||
},
|
||||
"history": {
|
||||
|
||||
@@ -18,10 +18,7 @@ import type { AreaRegistryEntry } from "./data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "./data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "./data/floor_registry";
|
||||
import type {
|
||||
CoreFrontendSystemData,
|
||||
CoreFrontendUserData,
|
||||
} from "./data/frontend";
|
||||
import type { CoreFrontendUserData } from "./data/frontend";
|
||||
import type {
|
||||
FrontendLocaleData,
|
||||
getHassTranslations,
|
||||
@@ -251,10 +248,10 @@ export interface HomeAssistant {
|
||||
vibrate: boolean;
|
||||
debugConnection: boolean;
|
||||
dockedSidebar: "docked" | "always_hidden" | "auto";
|
||||
defaultPanel: string;
|
||||
moreInfoEntityId: string | null;
|
||||
user?: CurrentUser;
|
||||
userData?: CoreFrontendUserData;
|
||||
systemData?: CoreFrontendSystemData;
|
||||
userData?: CoreFrontendUserData | null;
|
||||
hassUrl(path?): string;
|
||||
callService<T = any>(
|
||||
domain: ServiceCallRequest["domain"],
|
||||
|
||||
@@ -8,9 +8,8 @@ const STORED_STATE = [
|
||||
"debugConnection",
|
||||
"suspendWhenHidden",
|
||||
"enableShortcuts",
|
||||
] as const;
|
||||
|
||||
type StoredHomeAssistant = Pick<HomeAssistant, (typeof STORED_STATE)[number]>;
|
||||
"defaultPanel",
|
||||
];
|
||||
|
||||
export function storeState(hass: HomeAssistant) {
|
||||
try {
|
||||
@@ -32,8 +31,8 @@ export function storeState(hass: HomeAssistant) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getState(): Partial<StoredHomeAssistant> {
|
||||
const state = {} as Partial<StoredHomeAssistant>;
|
||||
export function getState() {
|
||||
const state = {};
|
||||
|
||||
STORED_STATE.forEach((key) => {
|
||||
const storageItem = window.localStorage.getItem(key);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("ha-pref-storage", () => {
|
||||
window.localStorage.setItem = vi.fn();
|
||||
|
||||
storeState(mockHass as unknown as HomeAssistant);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(7);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledTimes(8);
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
"dockedSidebar",
|
||||
JSON.stringify("auto")
|
||||
|
||||
48
yarn.lock
48
yarn.lock
@@ -1282,15 +1282,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/view@npm:6.38.8, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
|
||||
version: 6.38.8
|
||||
resolution: "@codemirror/view@npm:6.38.8"
|
||||
"@codemirror/view@npm:6.38.6, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
|
||||
version: 6.38.6
|
||||
resolution: "@codemirror/view@npm:6.38.6"
|
||||
dependencies:
|
||||
"@codemirror/state": "npm:^6.5.0"
|
||||
crelt: "npm:^1.0.6"
|
||||
style-mod: "npm:^4.1.0"
|
||||
w3c-keyname: "npm:^2.2.4"
|
||||
checksum: 10/81b1508015a378e4719d0239254173f0c5cd340c2abf96eb488fe5fb474bdb37ec1f010b9890ced774accd7aeb9443e7337cb6a89544b954273e5ddabece7cea
|
||||
checksum: 10/5a047337a98de111817ce8c8d39e6429c90ca0b0a4d2678d6e161e9e5961b1d476a891f447ab7a05cac395d4a93530e7c68bedd93191285265f0742a308ad00b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8869,25 +8869,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:12.0.0":
|
||||
version: 12.0.0
|
||||
resolution: "glob@npm:12.0.0"
|
||||
"glob@npm:11.0.3":
|
||||
version: 11.0.3
|
||||
resolution: "glob@npm:11.0.3"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.3.1"
|
||||
jackspeak: "npm:^4.1.1"
|
||||
minimatch: "npm:^10.1.1"
|
||||
minimatch: "npm:^10.0.3"
|
||||
minipass: "npm:^7.1.2"
|
||||
package-json-from-dist: "npm:^1.0.0"
|
||||
path-scurry: "npm:^2.0.0"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 10/6e21b3f1f1fa635836d45e54bbe50704884cc3e310e0cc011cfb5429db65a030e12936d99b07e66236370efe45dc8c8b26fa5334dbf555d6f8709e0315c77c30
|
||||
checksum: 10/2ae536c1360c0266b523b2bfa6aadc10144a8b7e08869b088e37ac3c27cd30774f82e4bfb291cde796776e878f9e13200c7ff44010eb7054e00f46f649397893
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.5.0":
|
||||
version: 10.5.0
|
||||
resolution: "glob@npm:10.5.0"
|
||||
"glob@npm:^10.2.2":
|
||||
version: 10.4.5
|
||||
resolution: "glob@npm:10.4.5"
|
||||
dependencies:
|
||||
foreground-child: "npm:^3.1.0"
|
||||
jackspeak: "npm:^3.1.2"
|
||||
@@ -8897,7 +8897,7 @@ __metadata:
|
||||
path-scurry: "npm:^1.11.1"
|
||||
bin:
|
||||
glob: dist/esm/bin.mjs
|
||||
checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653
|
||||
checksum: 10/698dfe11828b7efd0514cd11e573eaed26b2dff611f0400907281ce3eab0c1e56143ef9b35adc7c77ecc71fba74717b510c7c223d34ca8a98ec81777b293d4ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9208,7 +9208,7 @@ __metadata:
|
||||
"@codemirror/legacy-modes": "npm:6.5.2"
|
||||
"@codemirror/search": "npm:6.5.11"
|
||||
"@codemirror/state": "npm:6.5.2"
|
||||
"@codemirror/view": "npm:6.38.8"
|
||||
"@codemirror/view": "npm:6.38.6"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:6.18.2"
|
||||
@@ -9324,7 +9324,7 @@ __metadata:
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.3.2"
|
||||
fuse.js: "npm:7.1.0"
|
||||
glob: "npm:12.0.0"
|
||||
glob: "npm:11.0.3"
|
||||
google-timezones-json: "npm:1.2.0"
|
||||
gulp: "npm:5.0.1"
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
@@ -9337,7 +9337,7 @@ __metadata:
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:10.7.18"
|
||||
js-yaml: "npm:4.1.1"
|
||||
js-yaml: "npm:4.1.0"
|
||||
jsdom: "npm:27.1.0"
|
||||
jszip: "npm:3.10.1"
|
||||
leaflet: "npm:1.9.4"
|
||||
@@ -10407,14 +10407,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:4.1.1, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.1
|
||||
resolution: "js-yaml@npm:4.1.1"
|
||||
"js-yaml@npm:4.1.0, js-yaml@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "js-yaml@npm:4.1.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77
|
||||
checksum: 10/c138a34a3fd0d08ebaf71273ad4465569a483b8a639e0b118ff65698d257c2791d3199e3f303631f2cb98213fa7b5f5d6a4621fd0fff819421b990d30d967140
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11167,12 +11167,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^10.1.1":
|
||||
version: 10.1.1
|
||||
resolution: "minimatch@npm:10.1.1"
|
||||
"minimatch@npm:^10.0.3":
|
||||
version: 10.0.3
|
||||
resolution: "minimatch@npm:10.0.3"
|
||||
dependencies:
|
||||
"@isaacs/brace-expansion": "npm:^5.0.0"
|
||||
checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e
|
||||
checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user