mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-26 19:37:42 +00:00
Compare commits
10 Commits
ha-input
...
dialog-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1db214fd80 | ||
|
|
7581b7eb38 | ||
|
|
ab002ffc06 | ||
|
|
de59a558a7 | ||
|
|
28e562b98f | ||
|
|
7f175680ea | ||
|
|
f6a4c2cd33 | ||
|
|
43dba5fade | ||
|
|
7fd221d996 | ||
|
|
8107aa0ce2 |
@@ -8,6 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
@@ -16,7 +17,10 @@ import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import {
|
||||
showDialog,
|
||||
type ShowDialogParams,
|
||||
} from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
@@ -611,14 +615,15 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
};
|
||||
};
|
||||
|
||||
private _dialogManager = (e) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
private _dialogManager = (e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory, parentElement } =
|
||||
e.detail;
|
||||
showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
};
|
||||
|
||||
@@ -118,7 +118,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
makeDialogManager(this);
|
||||
|
||||
if (window.innerWidth > 450) {
|
||||
import("../../src/resources/particles");
|
||||
|
||||
40
package.json
40
package.json
@@ -34,10 +34,10 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.15",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.2",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.3",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -118,7 +118,7 @@
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.3",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -148,14 +148,14 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.56.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@html-eslint/eslint-plugin": "0.55.0",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.6",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
@@ -180,25 +180,25 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.6",
|
||||
"glob": "13.0.1",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.1.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -210,12 +210,12 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.9",
|
||||
"tar": "7.5.8",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.56.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.14.0"
|
||||
"node": "24.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,39 +210,3 @@ const formatDateWeekdayShortMem = memoizeOne(
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10
|
||||
export const formatDateWeekdayVeryShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayVeryShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
// Mon, Aug 10, 2021
|
||||
export const formatDateWeekdayShortDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -32,13 +32,11 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
private _lastFixedKeys: string[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = this._preserveLastFixed(params.columnOrder);
|
||||
this._columnOrder = params.columnOrder;
|
||||
this._hiddenColumns = params.hiddenColumns;
|
||||
this._open = true;
|
||||
}
|
||||
@@ -52,29 +50,6 @@ export class DialogDataTableSettings extends LitElement {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _lastFixedCount(): number {
|
||||
const lastFixedKeys = Object.keys(this._params!.columns).filter(
|
||||
(col) => this._params!.columns[col].lastFixed
|
||||
);
|
||||
if (lastFixedKeys.length) {
|
||||
this._lastFixedKeys = lastFixedKeys;
|
||||
}
|
||||
return lastFixedKeys.length;
|
||||
}
|
||||
|
||||
private _preserveLastFixed(columnOrder) {
|
||||
let strippedColumnOrder;
|
||||
const lastFixedCount = this._lastFixedCount();
|
||||
if (lastFixedCount && columnOrder) {
|
||||
strippedColumnOrder = [...columnOrder];
|
||||
strippedColumnOrder.splice(
|
||||
columnOrder.length - lastFixedCount,
|
||||
lastFixedCount
|
||||
);
|
||||
}
|
||||
return strippedColumnOrder;
|
||||
}
|
||||
|
||||
private _sortedColumns = memoizeOne(
|
||||
(
|
||||
columns: DataTableColumnContainer,
|
||||
@@ -82,7 +57,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
hiddenColumns: string[] | undefined
|
||||
) =>
|
||||
Object.keys(columns)
|
||||
.filter((col) => !columns[col].hidden && !columns[col].lastFixed)
|
||||
.filter((col) => !columns[col].hidden)
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder?.indexOf(a) ?? -1;
|
||||
const orderB = columnOrder?.indexOf(b) ?? -1;
|
||||
@@ -220,8 +195,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._columnOrder = columnOrder;
|
||||
|
||||
const reportedOrder = columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _toggle(ev) {
|
||||
@@ -302,8 +276,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
this._hiddenColumns = hidden;
|
||||
|
||||
const reportedOrder = this._columnOrder.concat(this._lastFixedKeys);
|
||||
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
private _reset() {
|
||||
|
||||
@@ -86,7 +86,6 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
flex?: number;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
lastFixed?: boolean;
|
||||
}
|
||||
|
||||
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
|
||||
@@ -136,6 +135,9 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property({ attribute: false }) public groupColumn?: string;
|
||||
@@ -357,11 +359,6 @@ export class HaDataTable extends LitElement {
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder!.indexOf(a);
|
||||
const orderB = columnOrder!.indexOf(b);
|
||||
const fixedA = Boolean(columns[a].lastFixed);
|
||||
const fixedB = Boolean(columns[b].lastFixed);
|
||||
if (fixedA !== fixedB) {
|
||||
return fixedA ? 1 : -1;
|
||||
}
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
@@ -397,6 +394,7 @@ export class HaDataTable extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
@@ -430,9 +428,9 @@ export class HaDataTable extends LitElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${!!this._checkedRows.length &&
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${!!this._checkedRows.length &&
|
||||
.checked=${this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { addBrandsAuth } from "../../util/brands-url";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
@@ -138,7 +137,6 @@ export class StateBadge extends LitElement {
|
||||
let imageUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
imageUrl = addBrandsAuth(imageUrl);
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
|
||||
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
}
|
||||
ha-markdown:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
|
||||
@@ -69,7 +69,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
if (this.hass && isIosApp(this.hass.auth.external)) {
|
||||
const element = this.renderRoot.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
|
||||
@@ -84,9 +84,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-primary-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-primary-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
@@ -102,9 +99,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-neutral-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-neutral-normal-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
@@ -120,9 +114,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-success-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-success-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
@@ -138,9 +129,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-warning-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-warning-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
@@ -156,9 +144,6 @@ export class HaButton extends Button {
|
||||
--button-color-fill-loud-hover: var(
|
||||
--ha-color-fill-danger-loud-hover
|
||||
);
|
||||
--button-color-fill-quiet-active: var(
|
||||
--ha-color-fill-danger-quiet-active
|
||||
);
|
||||
}
|
||||
|
||||
:host([appearance~="plain"]) .button {
|
||||
@@ -202,10 +187,6 @@ export class HaButton extends Button {
|
||||
background-color: var(--ha-color-fill-disabled-normal-resting);
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
}
|
||||
:host([appearance~="plain"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-quiet-active);
|
||||
}
|
||||
|
||||
:host([appearance~="accent"]) .button {
|
||||
background-color: var(
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { localizeContext } from "../data/context";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
|
||||
@customElement("ha-color-picker")
|
||||
export class HaColorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@@ -34,12 +34,15 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
render() {
|
||||
const effectiveValue = this.value ?? this.defaultColor ?? "";
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.hideClearIcon=${!this.value && !!this.defaultColor}
|
||||
@@ -50,7 +53,7 @@ export class HaColorPicker extends LitElement {
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
.notFoundLabel=${this.localize?.(
|
||||
"ui.components.color-picker.no_colors_found"
|
||||
)}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
@@ -78,7 +81,9 @@ export class HaColorPicker extends LitElement {
|
||||
return [
|
||||
{
|
||||
id: searchString,
|
||||
primary: this.hass.localize("ui.components.color-picker.custom_color"),
|
||||
primary:
|
||||
this.localize?.("ui.components.color-picker.custom_color") ||
|
||||
"Custom color",
|
||||
secondary: searchString,
|
||||
},
|
||||
];
|
||||
@@ -101,16 +106,15 @@ export class HaColorPicker extends LitElement {
|
||||
): PickerComboBoxItem[] => {
|
||||
const items: PickerComboBoxItem[] = [];
|
||||
|
||||
const defaultSuffix = this.hass.localize(
|
||||
"ui.components.color-picker.default"
|
||||
);
|
||||
const defaultSuffix =
|
||||
this.localize?.("ui.components.color-picker.default") || "Default";
|
||||
|
||||
const addDefaultSuffix = (label: string, isDefault: boolean) =>
|
||||
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
|
||||
|
||||
if (includeNone) {
|
||||
const noneLabel =
|
||||
this.hass.localize("ui.components.color-picker.none") || "None";
|
||||
this.localize?.("ui.components.color-picker.none") || "None";
|
||||
items.push({
|
||||
id: "none",
|
||||
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
|
||||
@@ -120,7 +124,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
if (includeState) {
|
||||
const stateLabel =
|
||||
this.hass.localize("ui.components.color-picker.state") || "State";
|
||||
this.localize?.("ui.components.color-picker.state") || "State";
|
||||
items.push({
|
||||
id: "state",
|
||||
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
|
||||
@@ -130,7 +134,7 @@ export class HaColorPicker extends LitElement {
|
||||
|
||||
Array.from(THEME_COLORS).forEach((color) => {
|
||||
const themeLabel =
|
||||
this.hass.localize(
|
||||
this.localize?.(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
) || color;
|
||||
items.push({
|
||||
@@ -184,7 +188,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.components.color-picker.none")}
|
||||
${this.localize?.("ui.components.color-picker.none") || "None"}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -192,7 +196,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.components.color-picker.state")}
|
||||
${this.localize?.("ui.components.color-picker.state") || "State"}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -200,7 +204,7 @@ export class HaColorPicker extends LitElement {
|
||||
return html`
|
||||
<span slot="start">${this._renderColorCircle(value)}</span>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
${this.localize?.(
|
||||
`ui.components.color-picker.colors.${value}` as LocalizeKeys
|
||||
) || value}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -11,9 +12,9 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { authContext, localizeContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -77,8 +78,6 @@ type DialogHideEvent = CustomEvent<{ source?: Element }>;
|
||||
*/
|
||||
@customElement("ha-dialog")
|
||||
export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@@ -117,6 +116,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: authContext, subscribe: true })
|
||||
private auth?: ContextType<typeof authContext>;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
@@ -162,7 +169,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.label=${this.localize?.("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
@@ -196,13 +203,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-dialog-autofocus";
|
||||
}
|
||||
this.hass?.auth.external?.fireMessage({
|
||||
this.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
@@ -334,29 +341,29 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
}
|
||||
|
||||
:host([type="standard"]) wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
:host([type="standard"]) wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
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);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
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);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -13,10 +14,9 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { authContext } from "../data/context";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
@@ -33,8 +33,6 @@ import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-generic-picker")
|
||||
export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@@ -113,6 +111,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
@state()
|
||||
@consume({ context: authContext, subscribe: true })
|
||||
private auth?: ContextType<typeof authContext>;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
@@ -142,10 +144,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._setUnknownValue();
|
||||
return;
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
this._throttleUnknownValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +250,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
return html`
|
||||
<ha-picker-combo-box
|
||||
id="combo-box"
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.searchLabel}
|
||||
.value=${this.value}
|
||||
@@ -291,13 +288,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
);
|
||||
};
|
||||
|
||||
private _throttleUnknownValue = throttle(
|
||||
this._setUnknownValue,
|
||||
1000,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
const showError = this.invalid && this.errorMessage;
|
||||
const showHelper = !showError && this.helper;
|
||||
@@ -321,8 +311,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
this._comboBox?.setFieldValue(this._initialFieldValue);
|
||||
this._initialFieldValue = undefined;
|
||||
}
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
this.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: "combo-box",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-icon";
|
||||
@@ -88,8 +88,6 @@ const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -111,7 +109,6 @@ export class HaIconPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
allow-custom-value
|
||||
.getItems=${this._getIconPickerItems}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
import "@home-assistant/webawesome/dist/components/input/input";
|
||||
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
|
||||
import { mdiClose, mdiEye, mdiEyeOff, mdiInformationOutline } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
/** The type of input. */
|
||||
@property()
|
||||
public type:
|
||||
| "date"
|
||||
| "datetime-local"
|
||||
| "email"
|
||||
| "number"
|
||||
| "password"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "text"
|
||||
| "time"
|
||||
| "url" = "text";
|
||||
|
||||
/** The current value of the input. */
|
||||
@property()
|
||||
public value: string | null = null;
|
||||
|
||||
/** The input's size. */
|
||||
@property()
|
||||
public size: "small" | "medium" | "large" = "medium";
|
||||
|
||||
/** The input's visual appearance. */
|
||||
@property()
|
||||
public appearance: "filled" | "outlined" | "filled-outlined" = "outlined";
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean })
|
||||
public pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
|
||||
/** The input's hint. */
|
||||
@property()
|
||||
public hint = "";
|
||||
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean, attribute: "with-clear" })
|
||||
public withClear = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property()
|
||||
public placeholder = "";
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. */
|
||||
@property({ type: Boolean, attribute: "password-toggle" })
|
||||
public passwordToggle = false;
|
||||
|
||||
/** Determines whether or not the password is currently visible. */
|
||||
@property({ type: Boolean, attribute: "password-visible" })
|
||||
public passwordVisible = false;
|
||||
|
||||
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
|
||||
@property({ type: Boolean, attribute: "without-spin-buttons" })
|
||||
public withoutSpinButtons = false;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property()
|
||||
public pattern?: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public min?: number | string;
|
||||
|
||||
/** The input's maximum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public max?: number | string;
|
||||
|
||||
/** Specifies the granularity that the value must adhere to. */
|
||||
@property()
|
||||
public step?: number | "any";
|
||||
|
||||
/** Controls whether and how text input is automatically capitalized. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = false;
|
||||
|
||||
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
/** Tells the browser what type of data will be entered by the user. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/** Disables the form control. */
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
/** Custom validation message to show when the input is invalid. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage = "";
|
||||
|
||||
/** When true, validates the input on blur instead of on form submit. */
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@state()
|
||||
private _invalid = false;
|
||||
|
||||
@query("wa-input")
|
||||
private _input!: WaInput;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
public select(): void {
|
||||
this._input?.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._input?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._input?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element. */
|
||||
public showPicker(): void {
|
||||
this._input?.showPicker();
|
||||
}
|
||||
|
||||
/** Increments the value of a numeric input type by the value of the step attribute. */
|
||||
public stepUp(): void {
|
||||
this._input?.stepUp();
|
||||
}
|
||||
|
||||
/** Decrements the value of a numeric input type by the value of the step attribute. */
|
||||
public stepDown(): void {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-input
|
||||
.type=${this.type}
|
||||
.value=${this.value}
|
||||
.size=${this.size}
|
||||
.appearance=${this.appearance}
|
||||
.hint=${this._invalid ? this.validationMessage : ""}
|
||||
.withClear=${this.withClear}
|
||||
.placeholder=${this.placeholder}
|
||||
.readonly=${this.readonly}
|
||||
.passwordToggle=${this.passwordToggle}
|
||||
.passwordVisible=${this.passwordVisible}
|
||||
.withoutSpinButtons=${this.withoutSpinButtons}
|
||||
.required=${this.required}
|
||||
.pattern=${this.pattern}
|
||||
.minlength=${this.minlength}
|
||||
.maxlength=${this.maxlength}
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.step=${this.step}
|
||||
.autocapitalize=${this.autocapitalize || undefined}
|
||||
.autocorrect=${this.autocorrect ? "on" : "off"}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autofocus=${this.autofocus}
|
||||
.enterkeyhint=${this.enterkeyhint || undefined}
|
||||
.spellcheck=${this.spellcheck}
|
||||
.inputmode=${this.inputmode || undefined}
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
class=${this._invalid ? "invalid" : ""}
|
||||
@input=${this._handleInput}
|
||||
@change=${this._handleChange}
|
||||
@blur=${this._handleBlur}
|
||||
>
|
||||
<div class="label" slot="label">
|
||||
<span>
|
||||
<slot name="label">${this.label}</slot>
|
||||
</span>
|
||||
${this.hint
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
id="hint"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="hint">${this.hint}</ha-tooltip> `
|
||||
: nothing}
|
||||
</div>
|
||||
<slot name="start" slot="start"></slot>
|
||||
<slot name="end" slot="end"></slot>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</slot>
|
||||
<slot name="show-password-icon" slot="show-password-icon">
|
||||
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
|
||||
</slot>
|
||||
<slot name="hide-password-icon" slot="hide-password-icon">
|
||||
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
||||
</slot>
|
||||
</wa-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? null;
|
||||
if (this._invalid) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange() {
|
||||
this.value = this._input?.value ?? null;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
if (this.autoValidate) {
|
||||
withViewTransition(() => {
|
||||
this._invalid = !this._input.checkValidity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
wa-input::part(base):focus-within {
|
||||
outline: none;
|
||||
--wa-form-control-border-color: var(--ha-color-border-primary-normal);
|
||||
}
|
||||
|
||||
wa-input.invalid {
|
||||
--wa-form-control-border-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
wa-input::part(label) {
|
||||
margin-block-end: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 24px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
|
||||
.label span {
|
||||
line-height: 1;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.label ha-svg-icon {
|
||||
color: var(--ha-color-on-disabled-normal);
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
wa-input.invalid::part(hint) {
|
||||
margin-block-start: var(--ha-space-1);
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
font-size: var(--ha-font-size-s);
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input": HaInput;
|
||||
}
|
||||
}
|
||||
@@ -84,11 +84,13 @@ export class HaMarkdown extends LitElement {
|
||||
ha-markdown-element > :is(ol, ul) {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
}
|
||||
li:has(input[type="checkbox"]) > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
@@ -135,10 +137,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: middle;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -14,6 +15,7 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { localeContext, localizeContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
@@ -21,7 +23,6 @@ import {
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isTouch } from "../util/is_touch";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
@@ -90,8 +91,6 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
||||
|
||||
@customElement("ha-picker-combo-box")
|
||||
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@@ -162,6 +161,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private locale!: ContextType<typeof localeContext>;
|
||||
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
@state() private _selectedSection?: string;
|
||||
@@ -215,9 +222,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
const searchLabel =
|
||||
this.label ??
|
||||
(this.allowCustomValue
|
||||
? (this.hass?.localize("ui.components.combo-box.search_or_custom") ??
|
||||
? (this.localize?.("ui.components.combo-box.search_or_custom") ??
|
||||
"Search | Add custom value")
|
||||
: (this.hass?.localize("ui.common.search") ?? "Search"));
|
||||
: (this.localize?.("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@@ -228,7 +235,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
|
||||
.label=${this.localize?.("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
@@ -350,7 +357,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return caseInsensitiveStringCompare(
|
||||
sortLabelA,
|
||||
sortLabelB,
|
||||
this.hass?.locale.language ?? navigator.language
|
||||
this.locale?.language ?? navigator.language
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -367,7 +374,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
id: this._search,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${this._search}"`,
|
||||
icon_path: mdiPlus,
|
||||
@@ -401,10 +408,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
? typeof this.notFoundLabel === "function"
|
||||
? this.notFoundLabel(this._search)
|
||||
: this.notFoundLabel ||
|
||||
this.hass?.localize("ui.components.combo-box.no_match") ||
|
||||
this.localize?.("ui.components.combo-box.no_match") ||
|
||||
"No matching items found"
|
||||
: this.emptyLabel ||
|
||||
this.hass?.localize("ui.components.combo-box.no_items") ||
|
||||
this.localize?.("ui.components.combo-box.no_items") ||
|
||||
"No items available"}</span
|
||||
>
|
||||
</ha-combo-box-item>
|
||||
@@ -507,7 +514,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
id: searchString,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
|
||||
this.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
|
||||
@@ -64,7 +64,7 @@ export class HaEntitySelector extends LitElement {
|
||||
if (!this.selector.entity?.multiple) {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${typeof this.value === "string" ? this.value : ""}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -13,11 +13,7 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
@@ -76,7 +72,16 @@ export class HaMediaSelector extends LitElement {
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else if (
|
||||
thumbnail &&
|
||||
thumbnail.startsWith("https://brands.home-assistant.io")
|
||||
) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
@@ -84,12 +89,6 @@ export class HaMediaSelector extends LitElement {
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems(options)}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
allow-custom-value
|
||||
></ha-generic-picker>
|
||||
@@ -231,7 +231,7 @@ export class HaSelectSelector extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ?? ""}
|
||||
.value=${typeof this.value === "string" ? this.value : ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -144,7 +144,6 @@ export const computePanels = memoizeOne(
|
||||
if (
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
panel.show_in_sidebar === false ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
|
||||
@@ -765,16 +765,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -797,6 +787,16 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,3 +34,5 @@ export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
||||
export const configEntriesContext =
|
||||
createContext<ConfigEntry[]>("configEntries");
|
||||
|
||||
export const authContext = createContext<HomeAssistant["auth"]>("auth");
|
||||
|
||||
@@ -1401,80 +1401,6 @@ export const calculateSolarConsumedGauge = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Conversion factors from each flow rate unit to L/min.
|
||||
* All HA-supported UnitOfVolumeFlowRate values are covered.
|
||||
*
|
||||
* m³/h → 1000/60 = 16.6667 L/min
|
||||
* m³/min → 1000 L/min
|
||||
* m³/s → 60000 L/min
|
||||
* ft³/min→ 28.3168 L/min
|
||||
* L/h → 1/60 L/min
|
||||
* L/min → 1 L/min
|
||||
* L/s → 60 L/min
|
||||
* gal/h → 3.78541/60 L/min
|
||||
* gal/min→ 3.78541 L/min
|
||||
* gal/d → 3.78541/1440 L/min
|
||||
* mL/s → 0.06 L/min
|
||||
*/
|
||||
|
||||
/** Exact number of liters in one US gallon */
|
||||
const LITERS_PER_GALLON = 3.785411784;
|
||||
|
||||
const FLOW_RATE_TO_LMIN: Record<string, number> = {
|
||||
"m³/h": 1000 / 60,
|
||||
"m³/min": 1000,
|
||||
"m³/s": 60000,
|
||||
"ft³/min": 28.316846592,
|
||||
"L/h": 1 / 60,
|
||||
"L/min": 1,
|
||||
"L/s": 60,
|
||||
"gal/h": LITERS_PER_GALLON / 60,
|
||||
"gal/min": LITERS_PER_GALLON,
|
||||
"gal/d": LITERS_PER_GALLON / 1440,
|
||||
"mL/s": 60 / 1000,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current flow rate from an entity state, converted to L/min.
|
||||
* @returns Flow rate in L/min, or undefined if unavailable/invalid.
|
||||
*/
|
||||
export const getFlowRateFromState = (
|
||||
stateObj?: HassEntity
|
||||
): number | undefined => {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
const value = parseFloat(stateObj.state);
|
||||
if (isNaN(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
const factor = unit ? FLOW_RATE_TO_LMIN[unit] : undefined;
|
||||
if (factor === undefined) {
|
||||
// Unknown unit – return raw value as-is (best effort)
|
||||
return value;
|
||||
}
|
||||
return value * factor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a flow rate value (in L/min) to a human-readable string using
|
||||
* the preferred unit system: metric → L/min, imperial → gal/min.
|
||||
*/
|
||||
export const formatFlowRateShort = (
|
||||
hassLocale: HomeAssistant["locale"],
|
||||
lengthUnitSystem: string,
|
||||
litersPerMin: number
|
||||
): string => {
|
||||
const isMetric = lengthUnitSystem === "km";
|
||||
if (isMetric) {
|
||||
return `${formatNumber(litersPerMin, hassLocale, { maximumFractionDigits: 1 })} L/min`;
|
||||
}
|
||||
const galPerMin = litersPerMin / LITERS_PER_GALLON;
|
||||
return `${formatNumber(galPerMin, hassLocale, { maximumFractionDigits: 1 })} gal/min`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to watts (W)
|
||||
* @param stateObj - The entity state object to get power value from
|
||||
|
||||
@@ -3,6 +3,7 @@ import { formatDurationDigital } from "../../common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
|
||||
// These attributes are hidden from the more-info window for all entities.
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"entity_id",
|
||||
"assumed_state",
|
||||
@@ -28,6 +29,8 @@ export const STATE_ATTRIBUTES = [
|
||||
"available_tones",
|
||||
];
|
||||
|
||||
// These attributes are hidden from the more-info window for entities of the
|
||||
// matching domain and device_class.
|
||||
export const STATE_ATTRIBUTES_DOMAIN_CLASS = {
|
||||
sensor: {
|
||||
enum: ["options"],
|
||||
|
||||
@@ -37,11 +37,6 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export interface LovelaceViewFooterConfig {
|
||||
card?: LovelaceCardConfig;
|
||||
column_span?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
sections?: LovelaceSectionConfig[];
|
||||
content_label?: string;
|
||||
@@ -73,7 +68,6 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
header?: LovelaceViewHeaderConfig;
|
||||
footer?: LovelaceViewFooterConfig;
|
||||
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||
sidebar?: LovelaceViewSidebarConfig;
|
||||
}
|
||||
|
||||
55
src/dialogs/dialog-mixin.ts
Normal file
55
src/dialogs/dialog-mixin.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaDialog } from "../components/ha-dialog";
|
||||
import type { Constructor } from "../types";
|
||||
import type { HassDialogNext } from "./make-dialog-manager";
|
||||
|
||||
export const DialogMixin = <
|
||||
P = unknown,
|
||||
T extends Constructor<LitElement> = Constructor<LitElement>,
|
||||
>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass implements HassDialogNext<P> {
|
||||
declare public params?: P;
|
||||
|
||||
private _closePromise?: Promise<boolean>;
|
||||
|
||||
private _closeResolve?: (value: boolean) => void;
|
||||
|
||||
public closeDialog(_historyState?: any): Promise<boolean> | boolean {
|
||||
if (this._closePromise) {
|
||||
return this._closePromise;
|
||||
}
|
||||
|
||||
const dialogElement = this.shadowRoot?.querySelector(
|
||||
"ha-dialog"
|
||||
) as HaDialog | null;
|
||||
if (dialogElement) {
|
||||
this._closePromise = new Promise<boolean>((resolve) => {
|
||||
this._closeResolve = resolve;
|
||||
});
|
||||
dialogElement.open = false;
|
||||
}
|
||||
return this._closePromise || true;
|
||||
}
|
||||
|
||||
private _removeDialog = (ev) => {
|
||||
ev.stopPropagation();
|
||||
this._closeResolve?.(true);
|
||||
this._closePromise = undefined;
|
||||
this._closeResolve = undefined;
|
||||
this.remove();
|
||||
};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("closed", this._removeDialog, { once: true });
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
this.removeEventListener("closed", this._removeDialog);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -19,18 +20,22 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HassDialog<
|
||||
T = HASSDomEvents[ValidHassDomEvent],
|
||||
> extends HTMLElement {
|
||||
export interface HassDialog<T = unknown> extends HTMLElement {
|
||||
showDialog(params: T);
|
||||
closeDialog?: (historyState?: any) => boolean;
|
||||
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
interface ShowDialogParams<T> {
|
||||
export interface HassDialogNext<T = unknown> extends HTMLElement {
|
||||
params?: T;
|
||||
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
|
||||
}
|
||||
|
||||
export interface ShowDialogParams<T> {
|
||||
dialogTag: keyof HTMLElementTagNameMap;
|
||||
dialogImport: () => Promise<unknown>;
|
||||
dialogParams: T;
|
||||
dialogParams?: T;
|
||||
addHistory?: boolean;
|
||||
parentElement?: LitElement;
|
||||
}
|
||||
|
||||
export interface DialogClosedParams {
|
||||
@@ -39,7 +44,6 @@ export interface DialogClosedParams {
|
||||
|
||||
export interface DialogState {
|
||||
element: HTMLElement & ProvideHassElement;
|
||||
root: ShadowRoot | HTMLElement;
|
||||
dialogTag: string;
|
||||
dialogParams: unknown;
|
||||
dialogImport?: () => Promise<unknown>;
|
||||
@@ -47,7 +51,7 @@ export interface DialogState {
|
||||
}
|
||||
|
||||
interface LoadedDialogInfo {
|
||||
element: Promise<HassDialog>;
|
||||
element: Promise<HassDialogNext | HassDialog> | null;
|
||||
closedFocusTargets?: Set<Element>;
|
||||
}
|
||||
|
||||
@@ -57,12 +61,24 @@ const LOADED: LoadedDialogsDict = {};
|
||||
const OPEN_DIALOG_STACK: DialogState[] = [];
|
||||
export const FOCUS_TARGET = Symbol.for("HA focus target");
|
||||
|
||||
/**
|
||||
* Shows a dialog element, lazy-loading it if needed, and optionally integrates
|
||||
* dialog open/close behavior with browser history.
|
||||
*
|
||||
* @param element The host element that can provide `hass` and receives the dialog by default.
|
||||
* @param dialogTag The custom element tag name of the dialog.
|
||||
* @param dialogParams The params passed to the dialog via `showDialog()` or `params`.
|
||||
* @param dialogImport Optional lazy import used when the dialog has not been loaded yet.
|
||||
* @param parentElement Optional parent to append the dialog to instead of root element.
|
||||
* @param addHistory Whether to add/update browser history so back navigation closes dialogs.
|
||||
* @returns `true` if the dialog was shown (or could be shown), `false` if it could not be loaded.
|
||||
*/
|
||||
export const showDialog = async (
|
||||
element: HTMLElement & ProvideHassElement,
|
||||
root: ShadowRoot | HTMLElement,
|
||||
element: LitElement & ProvideHassElement,
|
||||
dialogTag: string,
|
||||
dialogParams: unknown,
|
||||
dialogImport?: () => Promise<unknown>,
|
||||
parentElement?: LitElement,
|
||||
addHistory = true
|
||||
): Promise<boolean> => {
|
||||
if (!(dialogTag in LOADED)) {
|
||||
@@ -77,10 +93,18 @@ export const showDialog = async (
|
||||
}
|
||||
LOADED[dialogTag] = {
|
||||
element: dialogImport().then(() => {
|
||||
const dialogEl = document.createElement(dialogTag) as HassDialog;
|
||||
element.provideHass(dialogEl);
|
||||
const dialogEl = document.createElement(dialogTag) as
|
||||
| HassDialogNext
|
||||
| HassDialog;
|
||||
|
||||
if ("showDialog" in dialogEl) {
|
||||
// provide hass for legacy persistent dialogs
|
||||
element.provideHass(dialogEl);
|
||||
}
|
||||
|
||||
dialogEl.addEventListener("dialog-closed", _handleClosed);
|
||||
dialogEl.addEventListener("dialog-closed", _handleClosedFocus);
|
||||
|
||||
return dialogEl;
|
||||
}),
|
||||
};
|
||||
@@ -96,10 +120,10 @@ export const showDialog = async (
|
||||
});
|
||||
return showDialog(
|
||||
element,
|
||||
root,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
}
|
||||
@@ -111,7 +135,6 @@ export const showDialog = async (
|
||||
}
|
||||
OPEN_DIALOG_STACK.push({
|
||||
element,
|
||||
root,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
@@ -134,12 +157,24 @@ export const showDialog = async (
|
||||
FOCUS_TARGET
|
||||
);
|
||||
|
||||
const dialogElement = await LOADED[dialogTag].element;
|
||||
let dialogElement: HassDialogNext | HassDialog | null;
|
||||
|
||||
// Append it again so it's the last element in the root,
|
||||
// so it's guaranteed to be on top of the other elements
|
||||
root.appendChild(dialogElement);
|
||||
dialogElement.showDialog(dialogParams);
|
||||
if (LOADED[dialogTag] && LOADED[dialogTag].element === null) {
|
||||
dialogElement = document.createElement(dialogTag) as HassDialogNext;
|
||||
dialogElement.addEventListener("dialog-closed", _handleClosed);
|
||||
dialogElement.addEventListener("dialog-closed", _handleClosedFocus);
|
||||
LOADED[dialogTag].element = Promise.resolve(dialogElement);
|
||||
} else {
|
||||
dialogElement = await LOADED[dialogTag].element;
|
||||
}
|
||||
|
||||
if ("showDialog" in dialogElement!) {
|
||||
dialogElement.showDialog(dialogParams);
|
||||
} else {
|
||||
dialogElement!.params = dialogParams;
|
||||
}
|
||||
|
||||
(parentElement || element).shadowRoot!.appendChild(dialogElement!);
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -152,7 +187,7 @@ export const closeDialog = async (
|
||||
return true;
|
||||
}
|
||||
const dialogElement = await LOADED[dialogTag].element;
|
||||
if (dialogElement.closeDialog) {
|
||||
if (dialogElement && dialogElement.closeDialog) {
|
||||
return dialogElement.closeDialog(historyState) !== false;
|
||||
}
|
||||
return true;
|
||||
@@ -214,22 +249,34 @@ const _handleClosed = (ev: HASSDomEvent<DialogClosedParams>) => {
|
||||
mainWindow.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup element
|
||||
if (ev.currentTarget && "params" in ev.currentTarget) {
|
||||
const dialogElement = ev.currentTarget as HassDialogNext;
|
||||
dialogElement.removeEventListener("dialog-closed", _handleClosed);
|
||||
dialogElement.removeEventListener("dialog-closed", _handleClosedFocus);
|
||||
LOADED[ev.detail.dialog].element = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const makeDialogManager = (
|
||||
element: HTMLElement & ProvideHassElement,
|
||||
root: ShadowRoot | HTMLElement
|
||||
) => {
|
||||
export const makeDialogManager = (element: LitElement & ProvideHassElement) => {
|
||||
element.addEventListener(
|
||||
"show-dialog",
|
||||
(e: HASSDomEvent<ShowDialogParams<unknown>>) => {
|
||||
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
|
||||
const {
|
||||
dialogTag,
|
||||
dialogImport,
|
||||
dialogParams,
|
||||
addHistory,
|
||||
parentElement,
|
||||
} = e.detail;
|
||||
|
||||
showDialog(
|
||||
element,
|
||||
root,
|
||||
dialogTag,
|
||||
dialogParams,
|
||||
dialogImport,
|
||||
parentElement,
|
||||
addHistory
|
||||
);
|
||||
}
|
||||
|
||||
136
src/dialogs/more-info/ha-more-info-attributes.ts
Normal file
136
src/dialogs/more-info/ha-more-info-attributes.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface AttributesViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-attributes")
|
||||
class HaMoreInfoAttributes extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: AttributesViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const attributes = computeShownAttributes(this._stateObj);
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
${this._stateObj.attributes.attribution}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
margin-top: var(--ha-space-4);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-attributes": HaMoreInfoAttributes;
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display";
|
||||
import "../../components/ha-attribute-value";
|
||||
import "../../components/ha-card";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface DetailsViewParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
@customElement("ha-more-info-details")
|
||||
class HaMoreInfoDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public params?: DetailsViewParams;
|
||||
|
||||
@state() private _stateObj?: HassEntity;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("params") || changedProps.has("hass")) {
|
||||
if (this.params?.entityId && this.hass) {
|
||||
this._stateObj = this.hass.states[this.params.entityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params || !this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const translatedState = this.hass.formatEntityState(this._stateObj);
|
||||
const detailsAttributes = computeShownAttributes(this._stateObj);
|
||||
const detailsAttributeSet = new Set(detailsAttributes);
|
||||
const builtInAttributes = Object.keys(this._stateObj.attributes).filter(
|
||||
(attribute) => !detailsAttributeSet.has(attribute)
|
||||
);
|
||||
const allAttributes = [...detailsAttributes, ...builtInAttributes];
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
)}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.translated"
|
||||
)}
|
||||
</div>
|
||||
<div class="value">${translatedState}</div>
|
||||
</div>
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.raw")}
|
||||
</div>
|
||||
<div class="value">${this._stateObj.state}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize("ui.dialogs.more_info_control.attributes")}
|
||||
</h2>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<div class="attribute-group">
|
||||
${this._renderAttributes(allAttributes)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAttributes(attributes: string[]) {
|
||||
if (attributes.length === 0) {
|
||||
return html`<div class="empty">
|
||||
${this.hass.localize("ui.common.none")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return attributes.map(
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this._stateObj!,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
</div>
|
||||
<div class="value">
|
||||
<ha-attribute-value
|
||||
.hass=${this.hass}
|
||||
.attribute=${attribute}
|
||||
.stateObj=${this._stateObj}
|
||||
></ha-attribute-value>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6);
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6));
|
||||
}
|
||||
|
||||
.section + .section {
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.data-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.attribute-group .data-entry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
max-width: 60%;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-details": HaMoreInfoDetails;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-prev";
|
||||
import "../../components/ha-related-items";
|
||||
import { computeShownAttributes } from "../../data/entity/entity_attributes";
|
||||
import type {
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -82,6 +83,7 @@ export interface MoreInfoDialogParams {
|
||||
tab?: View;
|
||||
large?: boolean;
|
||||
data?: Record<string, any>;
|
||||
parentElement?: LitElement;
|
||||
}
|
||||
|
||||
type View = "info" | "history" | "settings" | "related" | "add_to";
|
||||
@@ -343,21 +345,31 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
case "info":
|
||||
this._resetInitialView();
|
||||
break;
|
||||
case "details":
|
||||
this._showDetails();
|
||||
case "attributes":
|
||||
this._showAttributes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _showDetails(): void {
|
||||
import("./ha-more-info-details");
|
||||
private _showAttributes(): void {
|
||||
import("./ha-more-info-attributes");
|
||||
this._childView = {
|
||||
viewTag: "ha-more-info-details",
|
||||
viewTitle: this.hass.localize("ui.dialogs.more_info_control.details"),
|
||||
viewTag: "ha-more-info-attributes",
|
||||
viewParams: { entityId: this._entityId },
|
||||
};
|
||||
}
|
||||
|
||||
private _hasDisplayableAttributes(): boolean {
|
||||
if (!this._entityId) {
|
||||
return false;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return computeShownAttributes(stateObj).length > 0;
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev))
|
||||
@@ -579,15 +591,19 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
"ui.dialogs.more_info_control.related"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="details">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.details"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
${this._hasDisplayableAttributes()
|
||||
? html`
|
||||
<ha-dropdown-item value="attributes">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListBulletedSquare}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.attributes"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._shouldShowAddEntityTo()
|
||||
? html`
|
||||
<ha-dropdown-item value="add_to">
|
||||
|
||||
@@ -146,7 +146,7 @@ export class QuickBar extends LitElement {
|
||||
private _dialogOpened = async () => {
|
||||
this._opened = true;
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
if (this.hass && isIosApp(this.hass.auth.external)) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import { DialogMixin } from "../dialog-mixin";
|
||||
|
||||
interface Text {
|
||||
textTranslationKey: LocalizeKeys;
|
||||
@@ -165,24 +166,10 @@ const _SHORTCUTS: Section[] = [
|
||||
];
|
||||
|
||||
@customElement("dialog-shortcuts")
|
||||
class DialogShortcuts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public async closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
class DialogShortcuts extends DialogMixin(LitElement) {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
private _renderShortcut(
|
||||
shortcutKeys: ShortcutString[],
|
||||
@@ -196,15 +183,13 @@ class DialogShortcuts extends LitElement {
|
||||
>${shortcutKey === CTRL_CMD
|
||||
? isMac
|
||||
? "⌘"
|
||||
: this.hass.localize("ui.dialogs.shortcuts.keys.ctrl")
|
||||
: this.localize("ui.dialogs.shortcuts.keys.ctrl")
|
||||
: typeof shortcutKey === "string"
|
||||
? shortcutKey
|
||||
: this.hass.localize(
|
||||
shortcutKey.shortcutTranslationKey
|
||||
)}</span
|
||||
: this.localize(shortcutKey.shortcutTranslationKey)}</span
|
||||
>`
|
||||
)}
|
||||
${this.hass.localize(descriptionKey)}
|
||||
${this.localize(descriptionKey)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -212,14 +197,13 @@ class DialogShortcuts extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
|
||||
open
|
||||
.headerTitle=${this.localize("ui.dialogs.shortcuts.title")}
|
||||
>
|
||||
<div class="content">
|
||||
${_SHORTCUTS.map(
|
||||
(section) => html`
|
||||
<h3>${this.hass.localize(section.titleTranslationKey)}</h3>
|
||||
<h3>${this.localize(section.titleTranslationKey)}</h3>
|
||||
<div class="items">
|
||||
${section.items.map((item) => {
|
||||
if ("shortcut" in item) {
|
||||
@@ -229,7 +213,7 @@ class DialogShortcuts extends LitElement {
|
||||
);
|
||||
}
|
||||
return html`<p>
|
||||
${this.hass.localize((item as Text).textTranslationKey)}
|
||||
${this.localize((item as Text).textTranslationKey)}
|
||||
</p>`;
|
||||
})}
|
||||
</div>
|
||||
@@ -238,9 +222,9 @@ class DialogShortcuts extends LitElement {
|
||||
</div>
|
||||
|
||||
<ha-alert slot="footer">
|
||||
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
${this.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
>${this.hass.localize(
|
||||
>${this.localize(
|
||||
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
|
||||
)}</a
|
||||
>`,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export const showShortcutsDialog = (element: HTMLElement) =>
|
||||
export const showShortcutsDialog = (element: LitElement) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-shortcuts",
|
||||
dialogImport: () => import("./dialog-shortcuts"),
|
||||
dialogParams: {},
|
||||
});
|
||||
|
||||
@@ -32,28 +32,21 @@ const initRouting = () => {
|
||||
new CacheFirst({ matchOptions: { ignoreSearch: true } })
|
||||
);
|
||||
|
||||
// Cache any brand images used for 1 day
|
||||
// Brands are proxied via the local API with backend caching.
|
||||
// Strip the rotating access token from cache keys so token rotation
|
||||
// doesn't bust the cache, while preserving other params like "placeholder".
|
||||
// Cache any brand images used for 30 days
|
||||
// Use revalidation so cache is always available during an extended outage
|
||||
registerRoute(
|
||||
({ url, request }) =>
|
||||
url.pathname.startsWith("/api/brands/") &&
|
||||
url.origin === "https://brands.home-assistant.io" &&
|
||||
request.destination === "image",
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: "brands",
|
||||
// CORS must be forced to work for CSS images
|
||||
fetchOptions: { mode: "cors", credentials: "omit" },
|
||||
plugins: [
|
||||
{
|
||||
cacheKeyWillBeUsed: async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.delete("token");
|
||||
return url.href;
|
||||
},
|
||||
},
|
||||
// Add 404 so we quickly respond to domains with missing images
|
||||
new CacheableResponsePlugin({ statuses: [0, 200, 404] }),
|
||||
new ExpirationPlugin({
|
||||
maxAgeSeconds: 60 * 60 * 24,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 30,
|
||||
purgeOnQuotaError: true,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -51,6 +51,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public supervisor = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
|
||||
@@ -320,6 +322,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.value=${id}
|
||||
.clickAction=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
@@ -380,6 +383,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
.route=${this.route}
|
||||
.tabs=${this.tabs}
|
||||
.mainPage=${this.mainPage}
|
||||
.supervisor=${this.supervisor}
|
||||
.pane=${showPane && this.showFilters}
|
||||
@sorting-changed=${this._sortingChanged}
|
||||
>
|
||||
@@ -485,6 +489,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.localize=${localize}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
|
||||
@@ -5,8 +5,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { restoreScroll } from "../common/decorators/restore-scroll";
|
||||
import { isNavigationClick } from "../common/dom/is-navigation-click";
|
||||
import { goBack, navigate } from "../common/navigate";
|
||||
import { goBack } from "../common/navigate";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
@@ -15,11 +14,6 @@ import "../components/ha-tab";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
|
||||
const normalizePathname = (pathname: string): string =>
|
||||
pathname.endsWith("/") && pathname.length > 1
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
|
||||
export interface PageNavigation {
|
||||
path: string;
|
||||
translationKey?: string;
|
||||
@@ -94,8 +88,9 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
return shownTabs.map(
|
||||
(page) => html`
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<a href=${page.path}>
|
||||
<ha-tab
|
||||
.hass=${this.hass}
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this.narrow}
|
||||
.name=${page.translationKey
|
||||
@@ -117,9 +112,8 @@ class HassTabsSubpage extends LitElement {
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("route")) {
|
||||
const currentPath = `${this.route.prefix}${this.route.path}`;
|
||||
this._activeTab = this.tabs.find((tab) =>
|
||||
this._isActiveTabPath(tab.path, currentPath)
|
||||
`${this.route.prefix}${this.route.path}`.includes(tab.path)
|
||||
);
|
||||
}
|
||||
super.willUpdate(changedProperties);
|
||||
@@ -215,36 +209,6 @@ class HassTabsSubpage extends LitElement {
|
||||
goBack();
|
||||
}
|
||||
|
||||
private _isActiveTabPath(tabPath: string, currentPath: string): boolean {
|
||||
try {
|
||||
const tabUrl = new URL(tabPath, window.location.origin);
|
||||
const currentUrl = new URL(currentPath, window.location.origin);
|
||||
|
||||
const tabPathname = normalizePathname(tabUrl.pathname);
|
||||
const currentPathname = normalizePathname(currentUrl.pathname);
|
||||
|
||||
if (
|
||||
currentPathname === tabPathname ||
|
||||
currentPathname.startsWith(`${tabPathname}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (_err) {
|
||||
return currentPath === tabPath || currentPath.startsWith(`${tabPath}/`);
|
||||
}
|
||||
}
|
||||
|
||||
private async _tabClicked(ev: MouseEvent): Promise<void> {
|
||||
const href = isNavigationClick(ev);
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
await navigate(href, { replace: true });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
|
||||
@@ -226,7 +226,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
||||
) {
|
||||
import("../resources/particles");
|
||||
}
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
makeDialogManager(this);
|
||||
import("../components/ha-language-picker");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { mdiMenu } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { HassioAddonDetails } from "../../data/hassio/addon";
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import "../../layouts/hass-loading-screen";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
|
||||
interface AppPanelConfig {
|
||||
@@ -43,7 +43,7 @@ class HaPanelApp extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public panel!: PanelInfo<AppPanelConfig>;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _addon?: HassioAddonDetails;
|
||||
|
||||
@@ -119,7 +119,7 @@ class HaPanelApp extends LitElement {
|
||||
${!this._kioskMode &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden")
|
||||
? html`
|
||||
<div class="header">
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
.path=${mdiMenu}
|
||||
@@ -130,10 +130,7 @@ class HaPanelApp extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<iframe
|
||||
class=${classMap({
|
||||
loaded: this._iframeLoaded,
|
||||
"kiosk-mode": this._kioskMode,
|
||||
})}
|
||||
class=${classMap({ loaded: this._iframeLoaded })}
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
@@ -454,16 +451,6 @@ class HaPanelApp extends LitElement {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
:host([narrow]) iframe {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
height: calc(100% - var(--safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
:host([narrow]) .header + iframe {
|
||||
padding-top: 0;
|
||||
height: calc(100% - 40px - var(--safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -479,11 +466,6 @@ class HaPanelApp extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
|
||||
:host([narrow]) .header {
|
||||
height: calc(40px + var(--safe-area-inset-top, 0px));
|
||||
padding-top: var(--safe-area-inset-top, 0);
|
||||
}
|
||||
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
|
||||
@@ -106,11 +106,12 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
filterable: true,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (credential) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -345,11 +345,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-icon-button
|
||||
.automation=${automation}
|
||||
|
||||
@@ -255,10 +255,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
type: "overflow-menu",
|
||||
template: (backup) => html`
|
||||
<ha-icon-button
|
||||
|
||||
@@ -232,11 +232,12 @@ class HaBlueprintOverview extends LitElement {
|
||||
hidden: true,
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (blueprint) =>
|
||||
blueprint.error
|
||||
? html`<ha-svg-icon
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-textfield";
|
||||
import type {
|
||||
CategoryRegistryEntry,
|
||||
CategoryRegistryEntryMutableParams,
|
||||
} from "../../../data/category_registry";
|
||||
import { localizeContext } from "../../../data/context";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
|
||||
|
||||
@customElement("dialog-category-registry-detail")
|
||||
class DialogCategoryDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
class DialogCategoryDetail extends DialogMixin<CategoryRegistryDetailDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@@ -26,53 +31,32 @@ class DialogCategoryDetail extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: CategoryRegistryDetailDialogParams;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
params: CategoryRegistryDetailDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || null;
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.params?.entry) {
|
||||
this._name = this.params.entry.name || "";
|
||||
this._icon = this.params.entry.icon || null;
|
||||
} else {
|
||||
this._name = this._params.suggestedName || "";
|
||||
this._name = this.params?.suggestedName || "";
|
||||
this._icon = null;
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._params.entry;
|
||||
const entry = this.params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
open
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||
: this.hass.localize("ui.panel.config.category.editor.create")}
|
||||
? this.localize("ui.panel.config.category.editor.edit")
|
||||
: this.localize("ui.panel.config.category.editor.create")}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -81,8 +65,8 @@ class DialogCategoryDetail extends LitElement {
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
|
||||
.validationMessage=${this.hass.localize(
|
||||
.label=${this.localize("ui.panel.config.category.editor.name")}
|
||||
.validationMessage=${this.localize(
|
||||
"ui.panel.config.category.editor.required_error_msg"
|
||||
)}
|
||||
required
|
||||
@@ -90,10 +74,9 @@ class DialogCategoryDetail extends LitElement {
|
||||
></ha-textfield>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
.value=${this._icon ?? undefined}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.icon")}
|
||||
.label=${this.localize("ui.panel.config.category.editor.icon")}
|
||||
></ha-icon-picker>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
@@ -102,7 +85,7 @@ class DialogCategoryDetail extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
${this.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@@ -110,8 +93,8 @@ class DialogCategoryDetail extends LitElement {
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
? this.localize("ui.common.save")
|
||||
: this.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
@@ -133,7 +116,7 @@ class DialogCategoryDetail extends LitElement {
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
const create = !this._params!.entry;
|
||||
const create = !this.params!.entry;
|
||||
this._submitting = true;
|
||||
let newValue: CategoryRegistryEntry | undefined;
|
||||
try {
|
||||
@@ -142,15 +125,15 @@ class DialogCategoryDetail extends LitElement {
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
};
|
||||
if (create) {
|
||||
newValue = await this._params!.createEntry!(values);
|
||||
newValue = await this.params!.createEntry!(values);
|
||||
} else {
|
||||
newValue = await this._params!.updateEntry!(values);
|
||||
newValue = await this.params!.updateEntry!(values);
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err.message ||
|
||||
this.hass.localize("ui.panel.config.category.editor.unknown_error");
|
||||
this.localize("ui.panel.config.category.editor.unknown_error");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../../components/ha-duration-input";
|
||||
import type { HaDurationData } from "../../../../components/ha-duration-input";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-input";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { ForDict } from "../../../../data/automation";
|
||||
import type { DurationDict, Timer } from "../../../../data/timer";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -66,21 +66,21 @@ class HaTimerForm extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="form">
|
||||
<ha-input
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@input=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.generic.name"
|
||||
)}
|
||||
auto-validate
|
||||
autoValidate
|
||||
required
|
||||
.validationMessage=${this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.required_error_msg"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
.disabled=${this.disabled}
|
||||
></ha-input>
|
||||
></ha-textfield>
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
|
||||
@@ -380,10 +380,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
localize("ui.panel.config.entities.picker.status.unmanageable")
|
||||
),
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
<ha-icon-overflow-menu
|
||||
|
||||
@@ -578,6 +578,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-integration-list-item
|
||||
brand
|
||||
.hass=${this.hass}
|
||||
.integration=${integration}
|
||||
tabindex="0"
|
||||
|
||||
@@ -30,6 +30,8 @@ export class HaIntegrationListItem extends ListItemBase {
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
@property({ type: Boolean }) hasMeta = true;
|
||||
|
||||
@property({ type: Boolean }) brand = false;
|
||||
|
||||
// @ts-expect-error
|
||||
protected override renderSingleLine() {
|
||||
if (!this.integration) {
|
||||
@@ -66,6 +68,7 @@ export class HaIntegrationListItem extends ListItemBase {
|
||||
domain: this.integration.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
brand: this.brand,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
|
||||
@@ -20,9 +20,9 @@ import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
const THREAD_ICON =
|
||||
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z";
|
||||
@@ -312,8 +312,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
a[slot="fab"] {
|
||||
|
||||
@@ -224,6 +224,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
slot="graphic"
|
||||
.src=${brandsUrl({
|
||||
domain: router.brand,
|
||||
brand: true,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
|
||||
@@ -10,7 +10,6 @@ import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
|
||||
import { changeZHANetworkChannel } from "../../../../../data/zha";
|
||||
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZHAChangeChannelDialogParams } from "./show-dialog-zha-change-channel";
|
||||
@@ -36,10 +35,7 @@ const VALID_CHANNELS = [
|
||||
];
|
||||
|
||||
@customElement("dialog-zha-change-channel")
|
||||
class DialogZHAChangeChannel
|
||||
extends LitElement
|
||||
implements HassDialog<ZHAChangeChannelDialogParams>
|
||||
{
|
||||
class DialogZHAChangeChannel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _migrationInProgress = false;
|
||||
@@ -50,24 +46,19 @@ class DialogZHAChangeChannel
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: ZHAChangeChannelDialogParams): void {
|
||||
public async showDialog(params: ZHAChangeChannelDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._newChannel = "auto";
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
if (this._migrationInProgress) {
|
||||
return false;
|
||||
}
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
this._newChannel = undefined;
|
||||
this._migrationInProgress = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -86,12 +77,7 @@ class DialogZHAChangeChannel
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.migration_warning_title"
|
||||
)}
|
||||
>
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.migration_warning"
|
||||
)}
|
||||
@@ -109,25 +95,25 @@ class DialogZHAChangeChannel
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.new_channel"
|
||||
)}
|
||||
autofocus
|
||||
@selected=${this._newChannelChosen}
|
||||
.value=${String(this._newChannel)}
|
||||
.options=${VALID_CHANNELS.map((channel) => ({
|
||||
value: String(channel),
|
||||
label:
|
||||
channel === "auto"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.channel_auto"
|
||||
)
|
||||
: String(channel),
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
|
||||
<p>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.new_channel"
|
||||
)}
|
||||
@selected=${this._newChannelChosen}
|
||||
.value=${String(this._newChannel)}
|
||||
.options=${VALID_CHANNELS.map((channel) => ({
|
||||
value: String(channel),
|
||||
label:
|
||||
channel === "auto"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.channel_auto"
|
||||
)
|
||||
: String(channel),
|
||||
}))}
|
||||
>
|
||||
</ha-select>
|
||||
</p>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
|
||||
@@ -6,10 +6,11 @@ import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-textarea";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { DEVICE_MESSAGE_TYPES, LOG_OUTPUT } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import "./zha-device-pairing-status-card";
|
||||
|
||||
@customElement("zha-add-devices-page")
|
||||
@@ -73,10 +74,11 @@ class ZHAAddDevicesPage extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.zha.add_device")}
|
||||
.route=${this.route!}
|
||||
.tabs=${zhaTabs}
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -166,7 +168,7 @@ class ZHAAddDevicesPage extends LitElement {
|
||||
>
|
||||
</ha-textarea>`
|
||||
: ""}
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,18 +39,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
tag: "zha-network-visualization-page",
|
||||
load: () => import("./zha-network-visualization-page"),
|
||||
},
|
||||
options: {
|
||||
tag: "zha-options-page",
|
||||
load: () => import("./zha-options-page"),
|
||||
},
|
||||
"network-info": {
|
||||
tag: "zha-network-info-page",
|
||||
load: () => import("./zha-network-info-page"),
|
||||
},
|
||||
section: {
|
||||
tag: "zha-config-section-page",
|
||||
load: () => import("./zha-config-section-page"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -65,8 +53,6 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
el.ieee = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "visualization") {
|
||||
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "section") {
|
||||
el.sectionId = this.routeTail.path.substr(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiCheck,
|
||||
mdiDevices,
|
||||
mdiDownload,
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiFolderMultipleOutline,
|
||||
mdiInformationOutline,
|
||||
mdiLan,
|
||||
mdiNetwork,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiShape,
|
||||
mdiTune,
|
||||
mdiVectorPolyline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-fab";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
@@ -31,16 +30,40 @@ import type {
|
||||
import {
|
||||
createZHANetworkBackup,
|
||||
fetchDevices,
|
||||
fetchGroups,
|
||||
fetchZHAConfiguration,
|
||||
fetchZHANetworkSettings,
|
||||
updateZHAConfiguration,
|
||||
} from "../../../../../data/zha";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { fileDownload } from "../../../../../util/file_download";
|
||||
import "../../../ha-config-section";
|
||||
import { showZHAChangeChannelDialog } from "./show-dialog-zha-change-channel";
|
||||
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
|
||||
|
||||
const MULTIPROTOCOL_ADDON_URL = "socket://core-silabs-multiprotocol:9999";
|
||||
|
||||
export const zhaTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.zha.network.caption",
|
||||
path: `/config/zha/dashboard`,
|
||||
iconPath: mdiNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.zha.groups.caption",
|
||||
path: `/config/zha/groups`,
|
||||
iconPath: mdiFolderMultipleOutline,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.zha.visualization.caption",
|
||||
path: `/config/zha/visualization`,
|
||||
iconPath: mdiLan,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("zha-config-dashboard")
|
||||
class ZHAConfigDashboard extends LitElement {
|
||||
@@ -56,25 +79,24 @@ class ZHAConfigDashboard extends LitElement {
|
||||
|
||||
@state() private _configuration?: ZHAConfiguration;
|
||||
|
||||
@state() private _networkSettings?: ZHANetworkSettings;
|
||||
|
||||
@state() private _totalDevices = 0;
|
||||
|
||||
@state() private _offlineDevices = 0;
|
||||
|
||||
@state() private _totalGroups = 0;
|
||||
|
||||
@state() private _networkSettings?: ZHANetworkSettings;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _generatingBackup = false;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this.hass.loadBackendTranslation("config_panel", "zha", false);
|
||||
this._fetchConfigEntry();
|
||||
this._fetchConfiguration();
|
||||
this._fetchSettings();
|
||||
this._fetchDevicesAndUpdateStatus();
|
||||
this._fetchGroups();
|
||||
this._fetchNetworkSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,17 +104,205 @@ class ZHAConfigDashboard extends LitElement {
|
||||
const deviceOnline =
|
||||
this._offlineDevices < this._totalDevices || this._totalDevices === 0;
|
||||
return html`
|
||||
<hass-subpage
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.zha.network.caption")}
|
||||
.route=${this.route}
|
||||
.tabs=${zhaTabs}
|
||||
back-path="/config"
|
||||
has-fab
|
||||
>
|
||||
<div class="container">
|
||||
${this._renderNetworkStatus(deviceOnline)}
|
||||
${this._renderMyNetworkCard()} ${this._renderNavigationCard()}
|
||||
${this._renderBackupCard()}
|
||||
<ha-card class="content network-status">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="card-content">
|
||||
<div class="heading">
|
||||
<div class="icon">
|
||||
<ha-svg-icon
|
||||
.path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle}
|
||||
class=${deviceOnline ? "online" : "offline"}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<div class="details">
|
||||
ZHA
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.status_title"
|
||||
)}:
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}`
|
||||
)}<br />
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.devices",
|
||||
{ count: this._totalDevices }
|
||||
)}
|
||||
</small>
|
||||
<small class="offline">
|
||||
${this._offlineDevices > 0
|
||||
? html`(${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.devices_offline",
|
||||
{ count: this._offlineDevices }
|
||||
)})`
|
||||
: nothing}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.caption"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.caption"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
class="network-settings"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.network_settings_title"
|
||||
)}
|
||||
>
|
||||
${this._networkSettings
|
||||
? html`<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<span slot="description">PAN ID</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.pan_id}</span
|
||||
>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.extended_pan_id}</span
|
||||
>
|
||||
<span slot="description">Extended PAN ID</span>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="description">Channel</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.channel}</span
|
||||
>
|
||||
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.change_channel"
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._showChannelMigrationDialog}
|
||||
>
|
||||
</ha-icon-button>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="description">Coordinator IEEE</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.settings.node_info.ieee}</span
|
||||
>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="description">Radio type</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.radio_type}</span
|
||||
>
|
||||
</ha-settings-row>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="description">Serial port</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.device.path}</span
|
||||
>
|
||||
</ha-settings-row>
|
||||
|
||||
${this._networkSettings.device.baudrate &&
|
||||
!this._networkSettings.device.path.startsWith("socket://")
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="description">Baudrate</span>
|
||||
<span slot="heading"
|
||||
>${this._networkSettings.device.baudrate}</span
|
||||
>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="plain"
|
||||
@click=${this._createAndDownloadBackup}
|
||||
.progress=${this._generatingBackup}
|
||||
.disabled=${!this._networkSettings || this._generatingBackup}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._openOptionFlow}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.migrate_radio"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._configuration
|
||||
? Object.entries(this._configuration.schemas).map(
|
||||
([section, schema]) =>
|
||||
html`<ha-card
|
||||
header=${this.hass.localize(
|
||||
`component.zha.config_panel.${section}.title`
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${schema}
|
||||
.data=${this._configuration!.data[section]}
|
||||
@value-changed=${this._dataChanged}
|
||||
.section=${section}
|
||||
.computeLabel=${this._computeLabelCallback(
|
||||
this.hass.localize,
|
||||
section
|
||||
)}
|
||||
></ha-form>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._updateConfiguration}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.update_button"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>`
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<a href="/config/zha/add" slot="fab">
|
||||
@@ -103,240 +313,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</a>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNetworkStatus(deviceOnline: boolean) {
|
||||
return html`
|
||||
<ha-card class="content network-status">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="card-content">
|
||||
<div class="heading">
|
||||
<div class="icon ${deviceOnline ? "success" : "error"}">
|
||||
<ha-svg-icon
|
||||
.path=${deviceOnline ? mdiCheck : mdiAlertCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<div class="details">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}`
|
||||
)}<br />
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.devices",
|
||||
{ count: this._totalDevices }
|
||||
)}
|
||||
</small>
|
||||
<small class="offline">
|
||||
${this._offlineDevices > 0
|
||||
? html`(${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.devices_offline",
|
||||
{ count: this._offlineDevices }
|
||||
)})`
|
||||
: nothing}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMyNetworkCard() {
|
||||
const deviceIds = this._configEntry
|
||||
? new Set(
|
||||
Object.values(this.hass.devices)
|
||||
.filter((device) =>
|
||||
device.config_entries.includes(this._configEntry!.entry_id)
|
||||
)
|
||||
.map((device) => device.id)
|
||||
)
|
||||
: new Set<string>();
|
||||
const entityCount = Object.values(this.hass.entities).filter(
|
||||
(entity) => entity.device_id && deviceIds.has(entity.device_id)
|
||||
).length;
|
||||
|
||||
return html`
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.my_network_title"
|
||||
)}
|
||||
<ha-button appearance="filled" href="/config/zha/visualization">
|
||||
<ha-svg-icon slot="start" .path=${mdiVectorPolyline}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.show_map"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.device_count",
|
||||
{ count: deviceIds.size }
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.entity_count",
|
||||
{ count: entityCount }
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item type="link" href="/config/zha/groups">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiFolderMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.group_count",
|
||||
{ count: this._totalGroups }
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNavigationCard() {
|
||||
const dynamicSections = this._configuration
|
||||
? Object.keys(this._configuration.schemas).filter(
|
||||
(section) => section !== "zha_options"
|
||||
)
|
||||
: [];
|
||||
|
||||
return html`
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item type="link" href="/config/zha/options">
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.options_title"
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.options_description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item type="link" href="/config/zha/network-info">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.network_info_title"
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.network_info_description"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
${dynamicSections.map(
|
||||
(section) => html`
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href=${`/config/zha/section/${section}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`component.zha.config_panel.${section}.title`
|
||||
) || section}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderBackupCard() {
|
||||
return html`
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="small"
|
||||
@click=${this._createAndDownloadBackup}
|
||||
.disabled=${!this._networkSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.migrate_radio"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.migrate_radio_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
size="small"
|
||||
@click=${this._openOptionFlow}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.migrate_radio_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -353,13 +330,45 @@ class ZHAConfigDashboard extends LitElement {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
}
|
||||
|
||||
private async _fetchNetworkSettings(): Promise<void> {
|
||||
private async _fetchSettings(): Promise<void> {
|
||||
this._networkSettings = await fetchZHANetworkSettings(this.hass!);
|
||||
}
|
||||
|
||||
private async _fetchDevicesAndUpdateStatus(): Promise<void> {
|
||||
try {
|
||||
const devices = await fetchDevices(this.hass);
|
||||
this._totalDevices = devices.length;
|
||||
this._offlineDevices =
|
||||
this._totalDevices - devices.filter((d) => d.available).length;
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _showChannelMigrationDialog(): Promise<void> {
|
||||
if (this._networkSettings!.device.path === MULTIPROTOCOL_ADDON_URL) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_dialog.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_dialog.text"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showZHAChangeChannelDialog(this, {
|
||||
currentChannel: this._networkSettings!.settings.network_info.channel,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createAndDownloadBackup(): Promise<void> {
|
||||
let backup_and_metadata: ZHANetworkBackupAndMetadata;
|
||||
|
||||
this._generatingBackup = true;
|
||||
|
||||
try {
|
||||
backup_and_metadata = await createZHANetworkBackup(this.hass!);
|
||||
} catch (err: any) {
|
||||
@@ -369,6 +378,8 @@ class ZHAConfigDashboard extends LitElement {
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
this._generatingBackup = false;
|
||||
}
|
||||
|
||||
if (!backup_and_metadata.is_complete) {
|
||||
@@ -400,24 +411,28 @@ class ZHAConfigDashboard extends LitElement {
|
||||
showOptionsFlowDialog(this, this._configEntry);
|
||||
}
|
||||
|
||||
private async _fetchGroups(): Promise<void> {
|
||||
private _dataChanged(ev) {
|
||||
this._configuration!.data[ev.currentTarget!.section] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateConfiguration(ev): Promise<any> {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
button.progress = true;
|
||||
try {
|
||||
const groups = await fetchGroups(this.hass);
|
||||
this._totalGroups = groups.length;
|
||||
} catch (_err) {
|
||||
// Groups are optional
|
||||
await updateZHAConfiguration(this.hass!, this._configuration!.data);
|
||||
button.actionSuccess();
|
||||
} catch (_err: any) {
|
||||
button.actionError();
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchDevicesAndUpdateStatus(): Promise<void> {
|
||||
try {
|
||||
const devices = await fetchDevices(this.hass);
|
||||
this._totalDevices = devices.length;
|
||||
this._offlineDevices =
|
||||
this._totalDevices - devices.filter((d) => d.available).length;
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
private _computeLabelCallback(localize, section: string) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(`component.zha.config_panel.${section}.${schema.name}`) ||
|
||||
schema.name;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -426,102 +441,75 @@ class ZHAConfigDashboard extends LitElement {
|
||||
css`
|
||||
ha-card {
|
||||
margin: auto;
|
||||
margin-top: var(--ha-space-4);
|
||||
max-width: 600px;
|
||||
margin-top: 16px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.nav-card .card-header {
|
||||
ha-card .card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
overflow: hidden;
|
||||
.network-settings ha-settings-row {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.nav-card .card-content {
|
||||
padding: 0;
|
||||
.network-settings ha-settings-row span[slot="heading"] {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
text-indent: -1em;
|
||||
padding-left: 1em;
|
||||
padding-inline-start: 1em;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
|
||||
.network-settings ha-settings-row ha-icon-button {
|
||||
margin-top: -16px;
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
ha-button[size="small"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.network-status div.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.network-status div.heading .icon {
|
||||
position: relative;
|
||||
border-radius: var(--ha-border-radius-2xl);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
--icon-color: var(--primary-color);
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
.network-status div.heading .icon.success {
|
||||
--icon-color: var(--success-color);
|
||||
}
|
||||
|
||||
.network-status div.heading .icon.error {
|
||||
--icon-color: var(--error-color);
|
||||
}
|
||||
|
||||
.network-status div.heading .icon::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--icon-color);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.network-status div.heading .icon ha-svg-icon {
|
||||
color: var(--icon-color);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.network-status div.heading ha-svg-icon {
|
||||
--mdc-icon-size: 48px;
|
||||
}
|
||||
|
||||
.network-status div.heading .details {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.network-status small {
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
|
||||
.network-status small.offline {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.network-status .online {
|
||||
color: var(--state-on-color, var(--success-color));
|
||||
}
|
||||
|
||||
.network-status .offline {
|
||||
color: var(--error-color, var(--error-color));
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { ZHAConfiguration } from "../../../../../data/zha";
|
||||
import {
|
||||
fetchZHAConfiguration,
|
||||
updateZHAConfiguration,
|
||||
} from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
@customElement("zha-config-section-page")
|
||||
class ZHAConfigSectionPage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: "section-id" }) public sectionId!: string;
|
||||
|
||||
@state() private _configuration?: ZHAConfiguration;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this.hass.loadBackendTranslation("config_panel", "zha", false);
|
||||
this._fetchConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchConfiguration(): Promise<void> {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const schema = this._configuration?.schemas[this.sectionId];
|
||||
const data = this._configuration?.data[this.sectionId];
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize(
|
||||
`component.zha.config_panel.${this.sectionId}.title`
|
||||
) || this.sectionId}
|
||||
back-path="/config/zha/dashboard"
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
${schema && data
|
||||
? html`
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${schema}
|
||||
.data=${data}
|
||||
@value-changed=${this._dataChanged}
|
||||
.computeLabel=${this._computeLabelCallback(
|
||||
this.hass.localize,
|
||||
this.sectionId
|
||||
)}
|
||||
></ha-form>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._updateConfiguration}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.update_button"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dataChanged(ev) {
|
||||
this._configuration!.data[this.sectionId] = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateConfiguration(ev: Event): Promise<void> {
|
||||
const button = ev.currentTarget as HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
actionError: () => void;
|
||||
};
|
||||
button.progress = true;
|
||||
try {
|
||||
await updateZHAConfiguration(this.hass!, this._configuration!.data);
|
||||
button.actionSuccess();
|
||||
} catch (_err: any) {
|
||||
button.actionError();
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabelCallback(localize, section: string) {
|
||||
return (schema) =>
|
||||
localize(`component.zha.config_panel.${section}.${schema.name}`) ||
|
||||
schema.name;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-config-section-page": ZHAConfigSectionPage;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiFolderMultipleOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -15,18 +15,10 @@ import "../../../../../components/ha-icon-button";
|
||||
import type { ZHAGroup } from "../../../../../data/zha";
|
||||
import { fetchGroups } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
|
||||
|
||||
const groupsTab: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.zha.groups.caption",
|
||||
path: "/config/zha/groups",
|
||||
iconPath: mdiFolderMultipleOutline,
|
||||
},
|
||||
];
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
|
||||
export interface GroupRowData extends ZHAGroup {
|
||||
group?: GroupRowData;
|
||||
@@ -108,8 +100,7 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.tabs=${groupsTab}
|
||||
back-path="/config/zha/dashboard"
|
||||
.tabs=${zhaTabs}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
import type {
|
||||
NetworkData,
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
function getLQIWidth(lqi: number): number {
|
||||
return lqi > 200 ? 3 : lqi > 100 ? 2 : 1;
|
||||
}
|
||||
|
||||
export function createZHANetworkChartData(
|
||||
devices: ZHADevice[],
|
||||
hass: HomeAssistant,
|
||||
element: Element
|
||||
): NetworkData {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
const primaryColor = style.getPropertyValue("--primary-color");
|
||||
const routerColor = style.getPropertyValue("--cyan-color");
|
||||
const endDeviceColor = style.getPropertyValue("--teal-color");
|
||||
const offlineColor = style.getPropertyValue("--error-color");
|
||||
const nodes: NetworkNode[] = [];
|
||||
const links: NetworkLink[] = [];
|
||||
const categories = [
|
||||
{
|
||||
name: hass.localize("ui.panel.config.zha.visualization.coordinator"),
|
||||
symbol: "roundRect",
|
||||
itemStyle: { color: primaryColor },
|
||||
},
|
||||
{
|
||||
name: hass.localize("ui.panel.config.zha.visualization.router"),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: routerColor },
|
||||
},
|
||||
{
|
||||
name: hass.localize("ui.panel.config.zha.visualization.end_device"),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: endDeviceColor },
|
||||
},
|
||||
{
|
||||
name: hass.localize("ui.panel.config.zha.visualization.offline"),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: offlineColor },
|
||||
},
|
||||
];
|
||||
|
||||
// Create all the nodes and links
|
||||
devices.forEach((device) => {
|
||||
const isCoordinator = device.device_type === "Coordinator";
|
||||
let category: number;
|
||||
if (!device.available) {
|
||||
category = 3; // Offline
|
||||
} else if (isCoordinator) {
|
||||
category = 0;
|
||||
} else if (device.device_type === "Router") {
|
||||
category = 1;
|
||||
} else {
|
||||
category = 2; // End Device
|
||||
}
|
||||
|
||||
const haDevice = hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = haDevice ? getDeviceContext(haDevice, hass).area : undefined;
|
||||
// Create node
|
||||
nodes.push({
|
||||
id: device.ieee,
|
||||
name: device.user_given_name || device.name || device.ieee,
|
||||
context: area?.name,
|
||||
category,
|
||||
value: isCoordinator ? 3 : device.device_type === "Router" ? 2 : 1,
|
||||
symbolSize: isCoordinator
|
||||
? 40
|
||||
: device.device_type === "Router"
|
||||
? 30
|
||||
: 20,
|
||||
symbol: isCoordinator ? "roundRect" : "circle",
|
||||
itemStyle: {
|
||||
color: device.available
|
||||
? isCoordinator
|
||||
? primaryColor
|
||||
: device.device_type === "Router"
|
||||
? routerColor
|
||||
: endDeviceColor
|
||||
: offlineColor,
|
||||
},
|
||||
polarDistance: category === 0 ? 0 : category === 1 ? 0.5 : 0.9,
|
||||
fixed: isCoordinator,
|
||||
});
|
||||
|
||||
// Create links (edges)
|
||||
const existingLinks = links.filter(
|
||||
(link) => link.source === device.ieee || link.target === device.ieee
|
||||
);
|
||||
if (device.routes && device.routes.length > 0) {
|
||||
device.routes.forEach((route) => {
|
||||
const neighbor = device.neighbors.find((n) => n.nwk === route.next_hop);
|
||||
if (!neighbor) {
|
||||
return;
|
||||
}
|
||||
const existingLink = existingLinks.find(
|
||||
(link) =>
|
||||
link.source === neighbor.ieee || link.target === neighbor.ieee
|
||||
);
|
||||
|
||||
if (existingLink) {
|
||||
if (existingLink.source === device.ieee) {
|
||||
existingLink.value = Math.max(
|
||||
existingLink.value!,
|
||||
parseInt(neighbor.lqi)
|
||||
);
|
||||
} else {
|
||||
existingLink.reverseValue = Math.max(
|
||||
existingLink.reverseValue ?? 0,
|
||||
parseInt(neighbor.lqi)
|
||||
);
|
||||
}
|
||||
const width = getLQIWidth(parseInt(neighbor.lqi));
|
||||
existingLink.symbolSize = (width / 4) * 6 + 3; // range 3-9
|
||||
existingLink.lineStyle = {
|
||||
...existingLink.lineStyle,
|
||||
width,
|
||||
color:
|
||||
route.route_status === "Active"
|
||||
? primaryColor
|
||||
: existingLink.lineStyle!.color,
|
||||
type: ["Child", "Parent"].includes(neighbor.relationship)
|
||||
? "solid"
|
||||
: existingLink.lineStyle!.type,
|
||||
};
|
||||
} else {
|
||||
// Create a new link
|
||||
const width = getLQIWidth(parseInt(neighbor.lqi));
|
||||
const link: NetworkLink = {
|
||||
source: device.ieee,
|
||||
target: neighbor.ieee,
|
||||
value: parseInt(neighbor.lqi),
|
||||
lineStyle: {
|
||||
width,
|
||||
color:
|
||||
route.route_status === "Active"
|
||||
? primaryColor
|
||||
: style.getPropertyValue("--dark-primary-color"),
|
||||
type: ["Child", "Parent"].includes(neighbor.relationship)
|
||||
? "solid"
|
||||
: "dotted",
|
||||
},
|
||||
symbolSize: (width / 4) * 6 + 3, // range 3-9
|
||||
// By default, all links should be ignored for force layout
|
||||
// unless it's a route to the coordinator
|
||||
ignoreForceLayout: route.dest_nwk !== "0x0000",
|
||||
};
|
||||
links.push(link);
|
||||
existingLinks.push(link);
|
||||
}
|
||||
});
|
||||
} else if (existingLinks.length === 0) {
|
||||
// If there are no links, create a link to the closest neighbor
|
||||
const neighbors: { ieee: string; lqi: string }[] = device.neighbors ?? [];
|
||||
if (neighbors.length === 0) {
|
||||
// If there are no neighbors, look for links from other devices
|
||||
devices.forEach((d) => {
|
||||
if (d.neighbors && d.neighbors.length > 0) {
|
||||
const neighbor = d.neighbors.find((n) => n.ieee === device.ieee);
|
||||
if (neighbor) {
|
||||
neighbors.push({ ieee: d.ieee, lqi: neighbor.lqi });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const closestNeighbor = neighbors.sort(
|
||||
(a, b) => parseInt(b.lqi) - parseInt(a.lqi)
|
||||
)[0];
|
||||
if (closestNeighbor) {
|
||||
links.push({
|
||||
source: device.ieee,
|
||||
target: closestNeighbor.ieee,
|
||||
value: parseInt(closestNeighbor.lqi),
|
||||
symbolSize: 5,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--dark-primary-color"),
|
||||
type: "dotted",
|
||||
},
|
||||
ignoreForceLayout: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now set ignoreForceLayout to false for the best connection of each device
|
||||
// Except for the coordinator which can have multiple strong connections
|
||||
devices.forEach((device) => {
|
||||
if (device.device_type === "Coordinator") {
|
||||
links.forEach((link) => {
|
||||
if (link.source === device.ieee || link.target === device.ieee) {
|
||||
link.ignoreForceLayout = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Find the link that corresponds to this strongest connection
|
||||
let bestLink: NetworkLink | undefined;
|
||||
const alreadyHasBestLink = links.some((link) => {
|
||||
if (link.source === device.ieee || link.target === device.ieee) {
|
||||
if (!link.ignoreForceLayout) {
|
||||
return true;
|
||||
}
|
||||
if (link.value! > (bestLink?.value ?? -1)) {
|
||||
bestLink = link;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!alreadyHasBestLink && bestLink) {
|
||||
bestLink.ignoreForceLayout = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes, links, categories };
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import type { ZHANetworkSettings } from "../../../../../data/zha";
|
||||
import { fetchZHANetworkSettings } from "../../../../../data/zha";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { showZHAChangeChannelDialog } from "./show-dialog-zha-change-channel";
|
||||
|
||||
const MULTIPROTOCOL_ADDON_URL = "socket://core-silabs-multiprotocol:9999";
|
||||
|
||||
@customElement("zha-network-info-page")
|
||||
class ZHANetworkInfoPage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _networkSettings?: ZHANetworkSettings;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this._fetchSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSettings(): Promise<void> {
|
||||
this._networkSettings = await fetchZHANetworkSettings(this.hass!);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.network_info_title"
|
||||
)}
|
||||
back-path="/config/zha/dashboard"
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
${this._networkSettings
|
||||
? html`<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.channel}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.change_channel"
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._showChannelMigrationDialog}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">PAN ID</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.pan_id}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Extended PAN ID</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.settings.network_info
|
||||
.extended_pan_id}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Coordinator IEEE</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.settings.node_info.ieee}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.radio_type"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.radio_type}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.serial_port"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.device.path}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
${this._networkSettings.device.baudrate &&
|
||||
!this._networkSettings.device.path.startsWith("socket://")
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.baudrate"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.device.baudrate}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _showChannelMigrationDialog(): Promise<void> {
|
||||
if (this._networkSettings!.device.path === MULTIPROTOCOL_ADDON_URL) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_dialog.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_dialog.text"
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showZHAChangeChannelDialog(this, {
|
||||
currentChannel: this._networkSettings!.settings.network_info.channel,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
--md-list-item-supporting-text-size: var(
|
||||
--md-list-item-label-text-size,
|
||||
var(--md-sys-typescale-body-large-size, 1rem)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-network-info-page": ZHANetworkInfoPage;
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,18 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { getDeviceContext } from "../../../../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
import type { NetworkData } from "../../../../../components/chart/ha-network-graph";
|
||||
import type {
|
||||
NetworkData,
|
||||
NetworkLink,
|
||||
NetworkNode,
|
||||
} from "../../../../../components/chart/ha-network-graph";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { ZHADevice } from "../../../../../data/zha";
|
||||
import { fetchDevices, refreshTopology } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import { createZHANetworkChartData } from "./zha-network-data";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
|
||||
@customElement("zha-network-visualization-page")
|
||||
export class ZHANetworkVisualizationPage extends LitElement {
|
||||
@@ -48,12 +52,13 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-subpage
|
||||
<hass-tabs-subpage
|
||||
.tabs=${zhaTabs}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.header"
|
||||
)}
|
||||
.isWide=${this.isWide}
|
||||
.route=${this.route}
|
||||
header=${this.hass.localize("ui.panel.config.zha.visualization.header")}
|
||||
>
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
@@ -71,17 +76,13 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</ha-network-graph>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
this._devices = await fetchDevices(this.hass!);
|
||||
this._networkData = createZHANetworkChartData(
|
||||
this._devices,
|
||||
this.hass,
|
||||
this
|
||||
);
|
||||
this._networkData = this._createChartData(this._devices);
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
@@ -157,6 +158,228 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _createChartData(devices: ZHADevice[]): NetworkData {
|
||||
const style = getComputedStyle(this);
|
||||
|
||||
const primaryColor = style.getPropertyValue("--primary-color");
|
||||
const routerColor = style.getPropertyValue("--cyan-color");
|
||||
const endDeviceColor = style.getPropertyValue("--teal-color");
|
||||
const offlineColor = style.getPropertyValue("--error-color");
|
||||
const nodes: NetworkNode[] = [];
|
||||
const links: NetworkLink[] = [];
|
||||
const categories = [
|
||||
{
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.coordinator"
|
||||
),
|
||||
symbol: "roundRect",
|
||||
itemStyle: { color: primaryColor },
|
||||
},
|
||||
{
|
||||
name: this.hass.localize("ui.panel.config.zha.visualization.router"),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: routerColor },
|
||||
},
|
||||
{
|
||||
name: this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.end_device"
|
||||
),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: endDeviceColor },
|
||||
},
|
||||
{
|
||||
name: this.hass.localize("ui.panel.config.zha.visualization.offline"),
|
||||
symbol: "circle",
|
||||
itemStyle: { color: offlineColor },
|
||||
},
|
||||
];
|
||||
|
||||
// Create all the nodes and links
|
||||
devices.forEach((device) => {
|
||||
const isCoordinator = device.device_type === "Coordinator";
|
||||
let category: number;
|
||||
if (!device.available) {
|
||||
category = 3; // Offline
|
||||
} else if (isCoordinator) {
|
||||
category = 0;
|
||||
} else if (device.device_type === "Router") {
|
||||
category = 1;
|
||||
} else {
|
||||
category = 2; // End Device
|
||||
}
|
||||
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = haDevice
|
||||
? getDeviceContext(haDevice, this.hass).area
|
||||
: undefined;
|
||||
// Create node
|
||||
nodes.push({
|
||||
id: device.ieee,
|
||||
name: device.user_given_name || device.name || device.ieee,
|
||||
context: area?.name,
|
||||
category,
|
||||
value: isCoordinator ? 3 : device.device_type === "Router" ? 2 : 1,
|
||||
symbolSize: isCoordinator
|
||||
? 40
|
||||
: device.device_type === "Router"
|
||||
? 30
|
||||
: 20,
|
||||
symbol: isCoordinator ? "roundRect" : "circle",
|
||||
itemStyle: {
|
||||
color: device.available
|
||||
? isCoordinator
|
||||
? primaryColor
|
||||
: device.device_type === "Router"
|
||||
? routerColor
|
||||
: endDeviceColor
|
||||
: offlineColor,
|
||||
},
|
||||
polarDistance: category === 0 ? 0 : category === 1 ? 0.5 : 0.9,
|
||||
fixed: isCoordinator,
|
||||
});
|
||||
|
||||
// Create links (edges)
|
||||
const existingLinks = links.filter(
|
||||
(link) => link.source === device.ieee || link.target === device.ieee
|
||||
);
|
||||
if (device.routes && device.routes.length > 0) {
|
||||
device.routes.forEach((route) => {
|
||||
const neighbor = device.neighbors.find(
|
||||
(n) => n.nwk === route.next_hop
|
||||
);
|
||||
if (!neighbor) {
|
||||
return;
|
||||
}
|
||||
const existingLink = existingLinks.find(
|
||||
(link) =>
|
||||
link.source === neighbor.ieee || link.target === neighbor.ieee
|
||||
);
|
||||
|
||||
if (existingLink) {
|
||||
if (existingLink.source === device.ieee) {
|
||||
existingLink.value = Math.max(
|
||||
existingLink.value!,
|
||||
parseInt(neighbor.lqi)
|
||||
);
|
||||
} else {
|
||||
existingLink.reverseValue = Math.max(
|
||||
existingLink.reverseValue ?? 0,
|
||||
parseInt(neighbor.lqi)
|
||||
);
|
||||
}
|
||||
const width = this._getLQIWidth(parseInt(neighbor.lqi));
|
||||
existingLink.symbolSize = (width / 4) * 6 + 3; // range 3-9
|
||||
existingLink.lineStyle = {
|
||||
...existingLink.lineStyle,
|
||||
width,
|
||||
color:
|
||||
route.route_status === "Active"
|
||||
? primaryColor
|
||||
: existingLink.lineStyle!.color,
|
||||
type: ["Child", "Parent"].includes(neighbor.relationship)
|
||||
? "solid"
|
||||
: existingLink.lineStyle!.type,
|
||||
};
|
||||
} else {
|
||||
// Create a new link
|
||||
const width = this._getLQIWidth(parseInt(neighbor.lqi));
|
||||
const link: NetworkLink = {
|
||||
source: device.ieee,
|
||||
target: neighbor.ieee,
|
||||
value: parseInt(neighbor.lqi),
|
||||
lineStyle: {
|
||||
width,
|
||||
color:
|
||||
route.route_status === "Active"
|
||||
? primaryColor
|
||||
: style.getPropertyValue("--dark-primary-color"),
|
||||
type: ["Child", "Parent"].includes(neighbor.relationship)
|
||||
? "solid"
|
||||
: "dotted",
|
||||
},
|
||||
symbolSize: (width / 4) * 6 + 3, // range 3-9
|
||||
// By default, all links should be ignored for force layout
|
||||
// unless it's a route to the coordinator
|
||||
ignoreForceLayout: route.dest_nwk !== "0x0000",
|
||||
};
|
||||
links.push(link);
|
||||
existingLinks.push(link);
|
||||
}
|
||||
});
|
||||
} else if (existingLinks.length === 0) {
|
||||
// If there are no links, create a link to the closest neighbor
|
||||
const neighbors: { ieee: string; lqi: string }[] =
|
||||
device.neighbors ?? [];
|
||||
if (neighbors.length === 0) {
|
||||
// If there are no neighbors, look for links from other devices
|
||||
devices.forEach((d) => {
|
||||
if (d.neighbors && d.neighbors.length > 0) {
|
||||
const neighbor = d.neighbors.find((n) => n.ieee === device.ieee);
|
||||
if (neighbor) {
|
||||
neighbors.push({ ieee: d.ieee, lqi: neighbor.lqi });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const closestNeighbor = neighbors.sort(
|
||||
(a, b) => parseInt(b.lqi) - parseInt(a.lqi)
|
||||
)[0];
|
||||
if (closestNeighbor) {
|
||||
links.push({
|
||||
source: device.ieee,
|
||||
target: closestNeighbor.ieee,
|
||||
value: parseInt(closestNeighbor.lqi),
|
||||
symbolSize: 5,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--dark-primary-color"),
|
||||
type: "dotted",
|
||||
},
|
||||
ignoreForceLayout: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now set ignoreForceLayout to false for the best connection of each device
|
||||
// Except for the coordinator which can have multiple strong connections
|
||||
devices.forEach((device) => {
|
||||
if (device.device_type === "Coordinator") {
|
||||
links.forEach((link) => {
|
||||
if (link.source === device.ieee || link.target === device.ieee) {
|
||||
link.ignoreForceLayout = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Find the link that corresponds to this strongest connection
|
||||
let bestLink: NetworkLink | undefined;
|
||||
const alreadyHasBestLink = links.some((link) => {
|
||||
if (link.source === device.ieee || link.target === device.ieee) {
|
||||
if (!link.ignoreForceLayout) {
|
||||
return true;
|
||||
}
|
||||
if (link.value! > (bestLink?.value ?? -1)) {
|
||||
bestLink = link;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!alreadyHasBestLink && bestLink) {
|
||||
bestLink.ignoreForceLayout = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes, links, categories };
|
||||
}
|
||||
|
||||
private _getLQIWidth(lqi: number): number {
|
||||
return lqi > 200 ? 3 : lqi > 100 ? 2 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,468 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { ZHAConfiguration } from "../../../../../data/zha";
|
||||
import {
|
||||
fetchZHAConfiguration,
|
||||
updateZHAConfiguration,
|
||||
} from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
const PREDEFINED_TIMEOUTS = [1800, 3600, 7200, 21600, 43200, 86400];
|
||||
|
||||
@customElement("zha-options-page")
|
||||
class ZHAOptionsPage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _configuration?: ZHAConfiguration;
|
||||
|
||||
@state() private _customMains = false;
|
||||
|
||||
@state() private _customBattery = false;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this._fetchConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchConfiguration(): Promise<void> {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
const mainsValue = this._configuration.data.zha_options
|
||||
?.consider_unavailable_mains as number | undefined;
|
||||
const batteryValue = this._configuration.data.zha_options
|
||||
?.consider_unavailable_battery as number | undefined;
|
||||
this._customMains =
|
||||
mainsValue !== undefined && !PREDEFINED_TIMEOUTS.includes(mainsValue);
|
||||
this._customBattery =
|
||||
batteryValue !== undefined && !PREDEFINED_TIMEOUTS.includes(batteryValue);
|
||||
}
|
||||
|
||||
private _getUnavailableTimeoutOptions(defaultSeconds: number) {
|
||||
const defaultLabel = ` (${this.hass.localize("ui.panel.config.zha.configuration_page.timeout_default")})`;
|
||||
const options: { value: string; seconds: number; key: string }[] = [
|
||||
{ value: "1800", seconds: 1800, key: "timeout_30_min" },
|
||||
{ value: "3600", seconds: 3600, key: "timeout_1_hour" },
|
||||
{ value: "7200", seconds: 7200, key: "timeout_2_hours" },
|
||||
{ value: "21600", seconds: 21600, key: "timeout_6_hours" },
|
||||
{ value: "43200", seconds: 43200, key: "timeout_12_hours" },
|
||||
{ value: "86400", seconds: 86400, key: "timeout_24_hours" },
|
||||
];
|
||||
return [
|
||||
...options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.zha.configuration_page.${opt.key}`,
|
||||
{ default: opt.seconds === defaultSeconds ? defaultLabel : "" }
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: "custom",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.timeout_custom"
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _getUnavailableDropdownValue(
|
||||
seconds: unknown,
|
||||
isCustom: boolean
|
||||
): string {
|
||||
if (isCustom) {
|
||||
return "custom";
|
||||
}
|
||||
const value = (seconds as number) ?? 7200;
|
||||
if (PREDEFINED_TIMEOUTS.includes(value)) {
|
||||
return String(value);
|
||||
}
|
||||
return "custom";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.options_title"
|
||||
)}
|
||||
back-path="/config/zha/dashboard"
|
||||
>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
${this._configuration
|
||||
? html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_identify_on_join_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_identify_on_join_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._configuration.data.zha_options
|
||||
?.enable_identify_on_join as boolean) ?? true}
|
||||
@change=${this._enableIdentifyOnJoinChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.default_light_transition_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.default_light_transition_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-textfield
|
||||
slot="end"
|
||||
type="number"
|
||||
.value=${String(
|
||||
(this._configuration.data.zha_options
|
||||
?.default_light_transition as number) ?? 0
|
||||
)}
|
||||
.suffix=${"s"}
|
||||
.min=${0}
|
||||
.step=${0.5}
|
||||
@change=${this._defaultLightTransitionChanged}
|
||||
></ha-textfield>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enhanced_light_transition_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enhanced_light_transition_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._configuration.data.zha_options
|
||||
?.enhanced_light_transition as boolean) ?? false}
|
||||
@change=${this._enhancedLightTransitionChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.light_transitioning_flag_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.light_transitioning_flag_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._configuration.data.zha_options
|
||||
?.light_transitioning_flag as boolean) ?? true}
|
||||
@change=${this._lightTransitioningFlagChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.group_members_assume_state_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.group_members_assume_state_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._configuration.data.zha_options
|
||||
?.group_members_assume_state as boolean) ?? true}
|
||||
@change=${this._groupMembersAssumeStateChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_mains_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_mains_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-select
|
||||
slot="end"
|
||||
.value=${this._getUnavailableDropdownValue(
|
||||
this._configuration.data.zha_options
|
||||
?.consider_unavailable_mains,
|
||||
this._customMains
|
||||
)}
|
||||
.options=${this._getUnavailableTimeoutOptions(7200)}
|
||||
@selected=${this._mainsUnavailableChanged}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
${this._customMains
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-textfield
|
||||
slot="end"
|
||||
type="number"
|
||||
.value=${String(
|
||||
(this._configuration.data.zha_options
|
||||
?.consider_unavailable_mains as number) ??
|
||||
7200
|
||||
)}
|
||||
.suffix=${"s"}
|
||||
.min=${1}
|
||||
.step=${1}
|
||||
@change=${this._customMainsSecondsChanged}
|
||||
></ha-textfield>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_battery_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_battery_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-select
|
||||
slot="end"
|
||||
.value=${this._getUnavailableDropdownValue(
|
||||
this._configuration.data.zha_options
|
||||
?.consider_unavailable_battery,
|
||||
this._customBattery
|
||||
)}
|
||||
.options=${this._getUnavailableTimeoutOptions(21600)}
|
||||
@selected=${this._batteryUnavailableChanged}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
${this._customBattery
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-textfield
|
||||
slot="end"
|
||||
type="number"
|
||||
.value=${String(
|
||||
(this._configuration.data.zha_options
|
||||
?.consider_unavailable_battery as number) ??
|
||||
21600
|
||||
)}
|
||||
.suffix=${"s"}
|
||||
.min=${1}
|
||||
.step=${1}
|
||||
@change=${this._customBatterySecondsChanged}
|
||||
></ha-textfield>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_mains_startup_polling_label"
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_mains_startup_polling_description"
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._configuration.data.zha_options
|
||||
?.enable_mains_startup_polling as boolean) ?? true}
|
||||
@change=${this._enableMainsStartupPollingChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._updateConfiguration}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.update_button"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _enableIdentifyOnJoinChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enable_identify_on_join = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enhancedLightTransitionChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enhanced_light_transition = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _lightTransitioningFlagChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.light_transitioning_flag = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _groupMembersAssumeStateChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.group_members_assume_state = checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _enableMainsStartupPollingChanged(ev: Event): void {
|
||||
const checked = (ev.target as HTMLInputElement).checked;
|
||||
this._configuration!.data.zha_options.enable_mains_startup_polling =
|
||||
checked;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _defaultLightTransitionChanged(ev: Event): void {
|
||||
const value = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.default_light_transition = value;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customMainsSecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains = seconds;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _customBatterySecondsChanged(ev: Event): void {
|
||||
const seconds = Number((ev.target as HTMLInputElement).value);
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery =
|
||||
seconds;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _mainsUnavailableChanged(ev: CustomEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (value === "custom") {
|
||||
this._customMains = true;
|
||||
} else {
|
||||
this._customMains = false;
|
||||
this._configuration!.data.zha_options.consider_unavailable_mains =
|
||||
Number(value);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _batteryUnavailableChanged(ev: CustomEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (value === "custom") {
|
||||
this._customBattery = true;
|
||||
} else {
|
||||
this._customBattery = false;
|
||||
this._configuration!.data.zha_options.consider_unavailable_battery =
|
||||
Number(value);
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private async _updateConfiguration(ev: Event): Promise<void> {
|
||||
const button = ev.currentTarget as HTMLElement & {
|
||||
progress: boolean;
|
||||
actionSuccess: () => void;
|
||||
actionError: () => void;
|
||||
};
|
||||
button.progress = true;
|
||||
try {
|
||||
await updateZHAConfiguration(this.hass!, this._configuration!.data);
|
||||
button.actionSuccess();
|
||||
} catch (_err: any) {
|
||||
button.actionError();
|
||||
} finally {
|
||||
button.progress = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
ha-select,
|
||||
ha-textfield {
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
ha-select,
|
||||
ha-textfield {
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-options-page": ZHAOptionsPage;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { goBack } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-fab";
|
||||
@@ -29,6 +28,7 @@ import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-progress-ring";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import { goBack } from "../../../../../common/navigate";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import {
|
||||
ERROR_STATES,
|
||||
@@ -968,8 +968,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4)
|
||||
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-color-picker";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-textarea";
|
||||
import "../../../components/ha-textfield";
|
||||
import { localizeContext } from "../../../data/context";
|
||||
import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LabelDetailDialogParams } from "./show-dialog-label-detail";
|
||||
|
||||
@customElement("dialog-label-detail")
|
||||
class DialogLabelDetail
|
||||
extends LitElement
|
||||
implements HassDialog<LabelDetailDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
class DialogLabelDetail extends DialogMixin<LabelDetailDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@@ -34,53 +35,35 @@ class DialogLabelDetail
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: LabelDetailDialogParams;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: LabelDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || "";
|
||||
this._color = this._params.entry.color || "";
|
||||
this._description = this._params.entry.description || "";
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.params?.entry) {
|
||||
this._name = this.params.entry.name || "";
|
||||
this._icon = this.params.entry.icon || "";
|
||||
this._color = this.params.entry.color || "";
|
||||
this._description = this.params.entry.description || "";
|
||||
} else {
|
||||
this._name = this._params.suggestedName || "";
|
||||
this._name = this.params?.suggestedName || "";
|
||||
this._icon = "";
|
||||
this._color = "";
|
||||
this._description = "";
|
||||
}
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.entry
|
||||
? this._params.entry.name || this._params.entry.label_id
|
||||
: this.hass!.localize("ui.dialogs.label-detail.new_label")}
|
||||
open
|
||||
header-title=${this.params.entry
|
||||
? this.params.entry.name || this.params.entry.label_id
|
||||
: this.localize("ui.dialogs.label-detail.new_label")}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
@@ -92,39 +75,35 @@ class DialogLabelDetail
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@input=${this._input}
|
||||
.label=${this.hass!.localize("ui.dialogs.label-detail.name")}
|
||||
.validationMessage=${this.hass!.localize(
|
||||
.label=${this.localize("ui.dialogs.label-detail.name")}
|
||||
.validationMessage=${this.localize(
|
||||
"ui.dialogs.label-detail.required_error_msg"
|
||||
)}
|
||||
required
|
||||
></ha-textfield>
|
||||
<ha-icon-picker
|
||||
.value=${this._icon}
|
||||
.hass=${this.hass}
|
||||
.configValue=${"icon"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize("ui.dialogs.label-detail.icon")}
|
||||
.label=${this.localize("ui.dialogs.label-detail.icon")}
|
||||
></ha-icon-picker>
|
||||
<ha-color-picker
|
||||
.value=${this._color}
|
||||
.configValue=${"color"}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize("ui.dialogs.label-detail.color")}
|
||||
.label=${this.localize("ui.dialogs.label-detail.color")}
|
||||
></ha-color-picker>
|
||||
<ha-textarea
|
||||
.value=${this._description}
|
||||
.configValue=${"description"}
|
||||
@input=${this._input}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.dialogs.label-detail.description"
|
||||
)}
|
||||
.label=${this.localize("ui.dialogs.label-detail.description")}
|
||||
></ha-textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
${this._params.entry && this._params.removeEntry
|
||||
${this.params.entry && this.params.removeEntry
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -133,7 +112,7 @@ class DialogLabelDetail
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
${this.localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
@@ -142,7 +121,7 @@ class DialogLabelDetail
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
${this.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
`}
|
||||
<ha-button
|
||||
@@ -150,9 +129,9 @@ class DialogLabelDetail
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting || !this._name}
|
||||
>
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.common.update")
|
||||
: this.hass!.localize("ui.common.create")}
|
||||
${this.params.entry
|
||||
? this.localize("ui.common.update")
|
||||
: this.localize("ui.common.create")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
@@ -184,10 +163,10 @@ class DialogLabelDetail
|
||||
color: this._color.trim() || null,
|
||||
description: this._description.trim() || null,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry!(values);
|
||||
if (this.params!.entry) {
|
||||
await this.params!.updateEntry!(values);
|
||||
} else {
|
||||
await this._params!.createEntry!(values);
|
||||
await this.params!.createEntry!(values);
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
@@ -200,8 +179,8 @@ class DialogLabelDetail
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry!()) {
|
||||
this._params = undefined;
|
||||
if (await this.params!.removeEntry!()) {
|
||||
this.params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
|
||||
@@ -147,10 +147,11 @@ export class HaConfigLabels extends LitElement {
|
||||
created_at: getCreatedAtTableColumn(localize, this.hass),
|
||||
modified_at: getModifiedAtTableColumn(localize, this.hass),
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
type: "overflow-menu",
|
||||
template: (label) => html`
|
||||
<ha-icon-button
|
||||
|
||||
@@ -344,7 +344,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
const item: DataTableItem = {
|
||||
icon: getPanelIcon(panelInfo),
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
show_in_sidebar: panelInfo.show_in_sidebar || false,
|
||||
show_in_sidebar: panelInfo.title != null,
|
||||
mode: "storage",
|
||||
url_path: panelInfo.url_path,
|
||||
filename: "",
|
||||
@@ -487,7 +487,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
icon: getPanelIcon(panelInfo),
|
||||
requireAdmin: panelInfo.require_admin || false,
|
||||
showInSidebar: panelInfo.show_in_sidebar || false,
|
||||
showInSidebar: panelInfo.title != null,
|
||||
isDefault: panelInfo.url_path === defaultPanel,
|
||||
updatePanel: async (values) => {
|
||||
await updatePanel(this.hass!, panelInfo.url_path, values);
|
||||
|
||||
@@ -143,8 +143,7 @@ class HaConfigRepairs extends LitElement {
|
||||
}
|
||||
} else if (
|
||||
issue.domain === "vacuum" &&
|
||||
(issue.translation_key === "segments_changed" ||
|
||||
issue.translation_key === "segments_mapping_not_configured")
|
||||
issue.translation_key === "segments_changed"
|
||||
) {
|
||||
const data = await fetchRepairsIssueData(
|
||||
this.hass.connection,
|
||||
|
||||
@@ -324,11 +324,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
localize("ui.panel.config.scene.picker.only_editable")
|
||||
),
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (scene) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -318,11 +318,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
labels: getLabelsTableColumn(),
|
||||
last_triggered: getTriggeredAtTableColumn(localize, this.hass),
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (script) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -21,12 +21,12 @@ export function getAssistantsTableColumn<T>(
|
||||
defaultHidden: !visible,
|
||||
sortable: true,
|
||||
showNarrow: true,
|
||||
minWidth: "112px",
|
||||
maxWidth: "112px",
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
valueColumn: "assistants_sortable_key",
|
||||
template: (entry: any) =>
|
||||
html`${entry.assistants.length !== 0
|
||||
? html`<div style="display: flex; gap: var(--ha-space-1);">
|
||||
? html`<div style="display: flex; gap: var(--ha-space-4);">
|
||||
${availableAssistants.map((vaId) => {
|
||||
const supported =
|
||||
!supportedEntities?.[vaId] ||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../../lovelace/strategies/helpers/screen-conditions";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
|
||||
@customElement("power-view-strategy")
|
||||
export class PowerViewStrategy extends ReactiveElement {
|
||||
@@ -19,6 +14,11 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [{ type: "grid", cards: [] }],
|
||||
};
|
||||
|
||||
const collectionKey =
|
||||
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
|
||||
|
||||
@@ -39,54 +39,16 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
const hasPowerDevices = prefs?.device_consumption.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
const hasWaterDevices = prefs?.device_consumption_water.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
const hasWaterSources = prefs?.energy_sources.some(
|
||||
(source) => source.type === "water" && source.stat_rate
|
||||
);
|
||||
const hasGasSources = prefs?.energy_sources.some(
|
||||
(source) => source.type === "gas" && source.stat_rate
|
||||
);
|
||||
|
||||
const tileSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [],
|
||||
column_span: 2,
|
||||
};
|
||||
const chartsSection: LovelaceSectionConfig = {
|
||||
type: "grid",
|
||||
cards: [],
|
||||
column_span: 2,
|
||||
};
|
||||
const tiles: LovelaceCardConfig[] = [];
|
||||
|
||||
const view: LovelaceViewConfig = {
|
||||
type: "sections",
|
||||
sections: [tileSection, chartsSection],
|
||||
max_columns: 2,
|
||||
};
|
||||
|
||||
// No sources configured
|
||||
if (
|
||||
!prefs ||
|
||||
(!hasPowerSources &&
|
||||
!hasPowerDevices &&
|
||||
!hasWaterDevices &&
|
||||
!hasWaterSources &&
|
||||
!hasGasSources)
|
||||
) {
|
||||
// No power sources configured
|
||||
if (!prefs || (!hasPowerSources && !hasPowerDevices)) {
|
||||
return view;
|
||||
}
|
||||
|
||||
if (hasPowerSources) {
|
||||
const card = {
|
||||
type: "power-total",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
tiles.push(card);
|
||||
const section = view.sections![0] as LovelaceSectionConfig;
|
||||
|
||||
chartsSection.cards!.push({
|
||||
if (hasPowerSources) {
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
|
||||
type: "power-sources-graph",
|
||||
collection_key: collectionKey,
|
||||
@@ -96,29 +58,13 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasGasSources) {
|
||||
const card = {
|
||||
type: "gas-total",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
tiles.push({ ...card });
|
||||
}
|
||||
|
||||
if (hasWaterSources) {
|
||||
const card = {
|
||||
type: "water-total",
|
||||
collection_key: collectionKey,
|
||||
};
|
||||
tiles.push({ ...card });
|
||||
}
|
||||
|
||||
if (hasPowerDevices) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption,
|
||||
hass,
|
||||
(d) => d.stat_rate
|
||||
);
|
||||
chartsSection.cards!.push({
|
||||
section.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
|
||||
type: "power-sankey",
|
||||
collection_key: collectionKey,
|
||||
@@ -130,41 +76,6 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWaterDevices) {
|
||||
const showFloorsAndAreas = shouldShowFloorsAndAreas(
|
||||
prefs.device_consumption_water,
|
||||
hass,
|
||||
(d) => d.stat_rate
|
||||
);
|
||||
chartsSection.cards!.push({
|
||||
title: hass.localize("ui.panel.energy.cards.water_flow_sankey_title"),
|
||||
type: "water-flow-sankey",
|
||||
collection_key: collectionKey,
|
||||
group_by_floor: showFloorsAndAreas,
|
||||
group_by_area: showFloorsAndAreas,
|
||||
grid_options: {
|
||||
columns: 36,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
tiles.forEach((card) => {
|
||||
tileSection.cards!.push({
|
||||
...card,
|
||||
grid_options: { columns: 24 / tiles.length },
|
||||
});
|
||||
});
|
||||
|
||||
if (tiles.length > 2) {
|
||||
// On small screens with 3 tiles, show them in 1 column
|
||||
tileSection.visibility = [LARGE_SCREEN_CONDITION];
|
||||
view.sections!.unshift({
|
||||
type: "grid",
|
||||
cards: tiles,
|
||||
visibility: [SMALL_SCREEN_CONDITION],
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,35 +109,18 @@ const processAreasForLight = (
|
||||
// Toggle group card for desktop
|
||||
cards.push({
|
||||
type: "toggle-group",
|
||||
title: hass.localize("ui.panel.lovelace.strategy.light.all_lights"),
|
||||
color: "amber",
|
||||
entities: areaLights,
|
||||
visibility: [LARGE_SCREEN_CONDITION],
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 1,
|
||||
min_columns: 6,
|
||||
},
|
||||
} as ToggleGroupCardConfig);
|
||||
|
||||
areaCards.forEach((card) => {
|
||||
// Insert a blank card before every 3rd card to align the individual
|
||||
// cards with the toggle group card on desktop
|
||||
if (
|
||||
areaCards.indexOf(card) % 3 === 0 &&
|
||||
areaCards.indexOf(card) !== 0
|
||||
) {
|
||||
cards.push({
|
||||
type: "vertical-stack",
|
||||
cards: [],
|
||||
visibility: [LARGE_SCREEN_CONDITION],
|
||||
grid_options: {
|
||||
columns: 6,
|
||||
rows: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
cards.push(card);
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import "../../../components/ha-svg-icon";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { addBrandsAuth } from "../../../util/brands-url";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
@@ -144,7 +143,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeStateDomain(stateObj) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
formatDateMonthYear,
|
||||
formatDateShort,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShortDate,
|
||||
formatDateWeekdayVeryShortDate,
|
||||
} from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
@@ -224,9 +222,7 @@ function formatTooltip(
|
||||
if (suggestedPeriod === "month") {
|
||||
period = `${formatDateMonthYear(date, locale, config)}`;
|
||||
} else if (suggestedPeriod === "day") {
|
||||
period = showCompareYear
|
||||
? formatDateWeekdayShortDate(date, locale, config)
|
||||
: formatDateWeekdayVeryShortDate(date, locale, config);
|
||||
period = `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}`;
|
||||
} else {
|
||||
period = `${
|
||||
compare
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { mdiFire } from "@mdi/js";
|
||||
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 "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/tile/ha-tile-container";
|
||||
import "../../../../components/tile/ha-tile-icon";
|
||||
import "../../../../components/tile/ha-tile-info";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
formatFlowRateShort,
|
||||
getEnergyDataCollection,
|
||||
getFlowRateFromState,
|
||||
} from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import { tileCardStyle } from "../tile/tile-card-style";
|
||||
import type { GasTotalCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-gas-total-card")
|
||||
export class HuiGasTotalCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: GasTotalCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: GasTotalCardConfig): void {
|
||||
this._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 1;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 1,
|
||||
min_rows: 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config") || changedProps.has("_data")) {
|
||||
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;
|
||||
}
|
||||
|
||||
private _getCurrentFlowRate(entityId: string): number {
|
||||
this._entities.add(entityId);
|
||||
return getFlowRateFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
private _computeTotalFlowRate(prefs: EnergyPreferences): number {
|
||||
this._entities.clear();
|
||||
|
||||
let totalFlow = 0;
|
||||
|
||||
prefs.energy_sources.forEach((source) => {
|
||||
if (source.type === "gas" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value > 0) totalFlow += value;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(0, totalFlow);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const flowRate = this._computeTotalFlowRate(this._data.prefs);
|
||||
const displayValue = formatFlowRateShort(
|
||||
this.hass.locale,
|
||||
this.hass.config.unit_system.length,
|
||||
flowRate
|
||||
);
|
||||
|
||||
const name =
|
||||
this._config.title ||
|
||||
this.hass.localize("ui.panel.lovelace.cards.energy.gas_total_title");
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-tile-container .interactive=${false}>
|
||||
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
|
||||
<ha-svg-icon slot="icon" .path=${mdiFire}></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info slot="info">
|
||||
<span slot="primary" class="primary">${name}</span>
|
||||
<span slot="secondary" class="secondary">${displayValue}</span>
|
||||
</ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--energy-gas-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-gas-total-card": HuiGasTotalCard;
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { mdiHomeLightningBolt } from "@mdi/js";
|
||||
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 { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/tile/ha-tile-container";
|
||||
import "../../../../components/tile/ha-tile-icon";
|
||||
import "../../../../components/tile/ha-tile-info";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getPowerFromState,
|
||||
} from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import { tileCardStyle } from "../tile/tile-card-style";
|
||||
import type { PowerTotalCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-power-total-card")
|
||||
export class HuiPowerTotalCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: PowerTotalCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: PowerTotalCardConfig): void {
|
||||
this._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 1;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 1,
|
||||
min_rows: 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config") || changedProps.has("_data")) {
|
||||
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;
|
||||
}
|
||||
|
||||
private _getCurrentPower(entityId: string): number {
|
||||
this._entities.add(entityId);
|
||||
return getPowerFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
private _computeTotalPower(prefs: EnergyPreferences): number {
|
||||
this._entities.clear();
|
||||
|
||||
let solar = 0;
|
||||
let from_grid = 0;
|
||||
let to_grid = 0;
|
||||
let from_battery = 0;
|
||||
let to_battery = 0;
|
||||
|
||||
prefs.energy_sources.forEach((source) => {
|
||||
if (source.type === "solar" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) solar += value;
|
||||
} else if (source.type === "grid" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) from_grid += value;
|
||||
else if (value < 0) to_grid += Math.abs(value);
|
||||
} else 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);
|
||||
}
|
||||
});
|
||||
|
||||
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
|
||||
return Math.max(0, used_total);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const power = this._computeTotalPower(this._data.prefs);
|
||||
|
||||
let displayValue = "";
|
||||
if (power >= 1000) {
|
||||
displayValue = `${formatNumber(power / 1000, this.hass.locale, {
|
||||
maximumFractionDigits: 2,
|
||||
})} kW`;
|
||||
} else {
|
||||
displayValue = `${formatNumber(power, this.hass.locale, {
|
||||
maximumFractionDigits: 0,
|
||||
})} W`;
|
||||
}
|
||||
|
||||
const name =
|
||||
this._config.title ||
|
||||
this.hass.localize("ui.panel.lovelace.cards.energy.power_total_title");
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-tile-container .interactive=${false}>
|
||||
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiHomeLightningBolt}
|
||||
></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info slot="info">
|
||||
<span slot="primary" class="primary">${name}</span>
|
||||
<span slot="secondary" class="secondary">${displayValue}</span>
|
||||
</ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-power-total-card": HuiPowerTotalCard;
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { mdiWater } from "@mdi/js";
|
||||
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 "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/tile/ha-tile-container";
|
||||
import "../../../../components/tile/ha-tile-icon";
|
||||
import "../../../../components/tile/ha-tile-info";
|
||||
import type { EnergyData, EnergyPreferences } from "../../../../data/energy";
|
||||
import {
|
||||
formatFlowRateShort,
|
||||
getEnergyDataCollection,
|
||||
getFlowRateFromState,
|
||||
} from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import { tileCardStyle } from "../tile/tile-card-style";
|
||||
import type { WaterTotalCardConfig } from "../types";
|
||||
|
||||
@customElement("hui-water-total-card")
|
||||
export class HuiWaterTotalCard
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: WaterTotalCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: WaterTotalCardConfig): void {
|
||||
this._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 1;
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
rows: 1,
|
||||
min_rows: 1,
|
||||
};
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("_config") || changedProps.has("_data")) {
|
||||
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;
|
||||
}
|
||||
|
||||
private _getCurrentFlowRate(entityId: string): number {
|
||||
this._entities.add(entityId);
|
||||
return getFlowRateFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
private _computeTotalFlowRate(prefs: EnergyPreferences): number {
|
||||
this._entities.clear();
|
||||
|
||||
let totalFlow = 0;
|
||||
|
||||
prefs.energy_sources.forEach((source) => {
|
||||
if (source.type === "water" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value > 0) totalFlow += value;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(0, totalFlow);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const flowRate = this._computeTotalFlowRate(this._data.prefs);
|
||||
const displayValue = formatFlowRateShort(
|
||||
this.hass.locale,
|
||||
this.hass.config.unit_system.length,
|
||||
flowRate
|
||||
);
|
||||
|
||||
const name =
|
||||
this._config.title ||
|
||||
this.hass.localize("ui.panel.lovelace.cards.energy.water_total_title");
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<ha-tile-container .interactive=${false}>
|
||||
<ha-tile-icon slot="icon" data-domain="sensor" data-state="active">
|
||||
<ha-svg-icon slot="icon" .path=${mdiWater}></ha-svg-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info slot="info">
|
||||
<span slot="primary" class="primary">${name}</span>
|
||||
<span slot="secondary" class="secondary">${displayValue}</span>
|
||||
</ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
tileCardStyle,
|
||||
css`
|
||||
:host {
|
||||
--tile-color: var(--energy-water-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-water-total-card": HuiWaterTotalCard;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { addBrandsAuth } from "../../../util/brands-url";
|
||||
import "../card-features/hui-card-features";
|
||||
import type { LovelaceCardFeatureContext } from "../card-features/types";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
@@ -159,7 +158,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(addBrandsAuth(entityPicture));
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeDomain(entity.entity_id) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class HuiToggleGroupCard extends LitElement implements LovelaceCard {
|
||||
return stateColorCss(onEntities[0]);
|
||||
}
|
||||
|
||||
private _computeLabel(): string {
|
||||
private _computeSecondary(): string {
|
||||
if (!this.hass || !this._config) return "";
|
||||
const onCount = this._getOnEntities().length;
|
||||
const total = this._config.entities.length;
|
||||
@@ -117,10 +117,6 @@ export class HuiToggleGroupCard extends LitElement implements LovelaceCard {
|
||||
const style = {
|
||||
"--tile-color": color,
|
||||
};
|
||||
|
||||
const label = this._computeLabel();
|
||||
const primary = this._config.title || label;
|
||||
const secondary = this._config.title ? label : undefined;
|
||||
return html`
|
||||
<ha-card style=${styleMap(style)}>
|
||||
<ha-tile-container .vertical=${Boolean(this._config.vertical)}>
|
||||
@@ -132,8 +128,8 @@ export class HuiToggleGroupCard extends LitElement implements LovelaceCard {
|
||||
></ha-tile-icon>
|
||||
<ha-tile-info
|
||||
slot="info"
|
||||
.primary=${primary}
|
||||
.secondary=${secondary}
|
||||
.primary=${this._config.title}
|
||||
.secondary=${this._computeSecondary()}
|
||||
></ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
|
||||
@@ -251,35 +251,12 @@ export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface WaterFlowSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "water-flow-sankey";
|
||||
title?: string;
|
||||
layout?: "vertical" | "horizontal" | "auto";
|
||||
group_by_floor?: boolean;
|
||||
group_by_area?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sources-graph";
|
||||
title?: string;
|
||||
show_legend?: boolean;
|
||||
}
|
||||
|
||||
export interface PowerTotalCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-total";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface WaterTotalCardConfig extends EnergyCardBaseConfig {
|
||||
type: "water-total";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface GasTotalCardConfig extends EnergyCardBaseConfig {
|
||||
type: "gas-total";
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface PowerSankeyCardConfig extends EnergyCardBaseConfig {
|
||||
type: "power-sankey";
|
||||
title?: string;
|
||||
|
||||
@@ -1,628 +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 { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
formatFlowRateShort,
|
||||
getEnergyDataCollection,
|
||||
getFlowRateFromState,
|
||||
} from "../../../../data/energy";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||
import type { WaterFlowSankeyCardConfig } 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 { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||
|
||||
const DEFAULT_CONFIG: Partial<WaterFlowSankeyCardConfig> = {
|
||||
group_by_floor: true,
|
||||
group_by_area: true,
|
||||
};
|
||||
|
||||
// Minimum flow threshold as a fraction of total inflow to display a device node.
|
||||
// Devices below this threshold will be grouped into an "Other" node.
|
||||
const MIN_FLOW_THRESHOLD_FACTOR = 0.001; // 0.1% of total inflow
|
||||
|
||||
interface SmallConsumer {
|
||||
statRate: string;
|
||||
name: string | undefined;
|
||||
value: number;
|
||||
effectiveParent: string | undefined;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
@customElement("hui-water-flow-sankey-card")
|
||||
class HuiWaterFlowSankeyCard
|
||||
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||
implements LovelaceCard
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
|
||||
@state() private _config?: WaterFlowSankeyCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
private _entities = new Set<string>();
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public setConfig(config: WaterFlowSankeyCardConfig): 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;
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (!oldHass || !this._entities.size) {
|
||||
return true;
|
||||
}
|
||||
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 computedStyle = getComputedStyle(this);
|
||||
|
||||
// Clear tracked entities and rebuild set
|
||||
this._entities.clear();
|
||||
|
||||
// Collect water sources with stat_rate
|
||||
const waterSources = prefs.energy_sources.filter(
|
||||
(source) => source.type === "water" && source.stat_rate
|
||||
);
|
||||
|
||||
let totalInflow = 0;
|
||||
waterSources.forEach((source) => {
|
||||
if (source.type === "water" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value > 0) totalInflow += value;
|
||||
}
|
||||
});
|
||||
|
||||
// When there are no source meters, pre-compute total device flow so the
|
||||
// home node has the correct value (sum of all device consumption) rather
|
||||
// than 0. This avoids a broken sankey where the root node has value=0
|
||||
// while its children have positive values.
|
||||
let totalDeviceFlow = 0;
|
||||
if (waterSources.length === 0) {
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
if (device.stat_rate) {
|
||||
totalDeviceFlow += this._getCurrentFlowRate(device.stat_rate);
|
||||
}
|
||||
});
|
||||
}
|
||||
const effectiveTotalInflow =
|
||||
waterSources.length === 0 ? totalDeviceFlow : totalInflow;
|
||||
|
||||
// Calculate dynamic threshold
|
||||
const minFlowThreshold = effectiveTotalInflow * MIN_FLOW_THRESHOLD_FACTOR;
|
||||
|
||||
const nodes: Node[] = [];
|
||||
const links: Link[] = [];
|
||||
const waterColor = computedStyle
|
||||
.getPropertyValue("--energy-water-color")
|
||||
.trim();
|
||||
const primaryColor = computedStyle
|
||||
.getPropertyValue("--primary-color")
|
||||
.trim();
|
||||
|
||||
// Determine the "root" node for device links.
|
||||
// - 0 sources: home node (value = sum of device values, computed later)
|
||||
// - 1 source: that source node is the root (no home node)
|
||||
// - >1 sources: home node aggregates all sources
|
||||
const showHomeNode = waterSources.length !== 1;
|
||||
let rootNodeId: string;
|
||||
|
||||
if (showHomeNode) {
|
||||
// Add source nodes and link to home
|
||||
waterSources.forEach((source) => {
|
||||
if (source.type !== "water" || !source.stat_rate) return;
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
if (value <= 0) return;
|
||||
const sourceNodeId = `water_source_${source.stat_rate}`;
|
||||
nodes.push({
|
||||
id: sourceNodeId,
|
||||
label:
|
||||
this._getEntityLabel(source.stat_rate) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.water"
|
||||
),
|
||||
value,
|
||||
color: waterColor,
|
||||
index: 0,
|
||||
entityId: source.stat_rate,
|
||||
});
|
||||
links.push({ source: sourceNodeId, target: "home" });
|
||||
});
|
||||
|
||||
const homeNode: Node = {
|
||||
id: "home",
|
||||
label: this.hass.config.location_name,
|
||||
value: Math.max(0, effectiveTotalInflow),
|
||||
color: primaryColor,
|
||||
index: 1,
|
||||
};
|
||||
nodes.push(homeNode);
|
||||
rootNodeId = "home";
|
||||
} else {
|
||||
// Single source: that source IS the root, no home node
|
||||
const source = waterSources[0];
|
||||
if (source.type === "water" && source.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(source.stat_rate);
|
||||
nodes.push({
|
||||
id: source.stat_rate,
|
||||
label:
|
||||
this._getEntityLabel(source.stat_rate) ||
|
||||
this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_distribution.water"
|
||||
),
|
||||
value: Math.max(0, value),
|
||||
color: waterColor,
|
||||
index: 0,
|
||||
entityId: source.stat_rate,
|
||||
});
|
||||
rootNodeId = source.stat_rate;
|
||||
} else {
|
||||
// Fallback (shouldn't happen)
|
||||
rootNodeId = "home";
|
||||
}
|
||||
}
|
||||
|
||||
// Build a map of device relationships for hierarchy resolution
|
||||
const deviceMap = new Map<
|
||||
string,
|
||||
{ stat_rate?: string; included_in_stat?: string }
|
||||
>();
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
deviceMap.set(device.stat_consumption, {
|
||||
stat_rate: device.stat_rate,
|
||||
included_in_stat: device.included_in_stat,
|
||||
});
|
||||
});
|
||||
|
||||
// Set of stat_rate entities that will be rendered as nodes
|
||||
const renderedStatRates = new Set<string>();
|
||||
prefs.device_consumption_water.forEach((device) => {
|
||||
if (device.stat_rate) {
|
||||
const value = this._getCurrentFlowRate(device.stat_rate);
|
||||
if (value >= minFlowThreshold) {
|
||||
renderedStatRates.add(device.stat_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find the effective parent for hierarchy
|
||||
const findEffectiveParent = (
|
||||
includedInStat: string | undefined
|
||||
): string | undefined => {
|
||||
let currentParent = includedInStat;
|
||||
while (currentParent) {
|
||||
const parentDevice = deviceMap.get(currentParent);
|
||||
if (!parentDevice) return undefined;
|
||||
if (
|
||||
parentDevice.stat_rate &&
|
||||
renderedStatRates.has(parentDevice.stat_rate)
|
||||
) {
|
||||
return parentDevice.stat_rate;
|
||||
}
|
||||
currentParent = parentDevice.included_in_stat;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let untrackedConsumption = effectiveTotalInflow;
|
||||
const deviceNodes: Node[] = [];
|
||||
const parentLinks: Record<string, string> = {};
|
||||
const smallConsumersByParent = new Map<string, SmallConsumer[]>();
|
||||
|
||||
prefs.device_consumption_water.forEach((device, idx) => {
|
||||
if (!device.stat_rate) return;
|
||||
const value = this._getCurrentFlowRate(device.stat_rate);
|
||||
const effectiveParent = findEffectiveParent(device.included_in_stat);
|
||||
|
||||
if (value < minFlowThreshold) {
|
||||
const parentKey = effectiveParent ?? rootNodeId;
|
||||
if (!smallConsumersByParent.has(parentKey)) {
|
||||
smallConsumersByParent.set(parentKey, []);
|
||||
}
|
||||
smallConsumersByParent.get(parentKey)!.push({
|
||||
statRate: device.stat_rate,
|
||||
name: device.name,
|
||||
value,
|
||||
effectiveParent,
|
||||
idx,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: device.stat_rate,
|
||||
label: device.name || this._getEntityLabel(device.stat_rate),
|
||||
value,
|
||||
color: getGraphColorByIndex(idx, computedStyle),
|
||||
index: 4,
|
||||
parent: effectiveParent,
|
||||
entityId: device.stat_rate,
|
||||
};
|
||||
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({ source: node.parent, target: node.id });
|
||||
} else {
|
||||
untrackedConsumption -= value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
});
|
||||
|
||||
// Process small consumers
|
||||
smallConsumersByParent.forEach((consumers, parentKey) => {
|
||||
const totalValue = consumers.reduce((sum, c) => sum + c.value, 0);
|
||||
if (totalValue <= 0) return;
|
||||
|
||||
if (consumers.length === 1) {
|
||||
const consumer = consumers[0];
|
||||
const node = {
|
||||
id: consumer.statRate,
|
||||
label: consumer.name || this._getEntityLabel(consumer.statRate),
|
||||
value: consumer.value,
|
||||
color: getGraphColorByIndex(consumer.idx, computedStyle),
|
||||
index: 4,
|
||||
parent: consumer.effectiveParent,
|
||||
entityId: consumer.statRate,
|
||||
};
|
||||
if (node.parent) {
|
||||
parentLinks[node.id] = node.parent;
|
||||
links.push({ source: node.parent, target: node.id });
|
||||
} else {
|
||||
untrackedConsumption -= consumer.value;
|
||||
}
|
||||
deviceNodes.push(node);
|
||||
} else {
|
||||
const otherNodeId = `other_${parentKey}`;
|
||||
const otherNode: Node = {
|
||||
id: otherNodeId,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.other"
|
||||
),
|
||||
value: Math.ceil(totalValue),
|
||||
color: computedStyle
|
||||
.getPropertyValue("--state-unavailable-color")
|
||||
.trim(),
|
||||
index: 4,
|
||||
};
|
||||
|
||||
if (parentKey !== rootNodeId) {
|
||||
parentLinks[otherNodeId] = parentKey;
|
||||
links.push({ source: parentKey, target: otherNodeId });
|
||||
} else {
|
||||
untrackedConsumption -= totalValue;
|
||||
}
|
||||
deviceNodes.push(otherNode);
|
||||
}
|
||||
});
|
||||
|
||||
const devicesWithoutParent = deviceNodes.filter(
|
||||
(node) => !parentLinks[node.id]
|
||||
);
|
||||
|
||||
const { group_by_area, group_by_floor, layout, title } = 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) {
|
||||
floorNodeId = rootNodeId;
|
||||
} else {
|
||||
nodes.push({
|
||||
id: floorNodeId,
|
||||
label: this.hass.floors[floorId].name,
|
||||
value: floors[floorId].value,
|
||||
index: 2,
|
||||
color: primaryColor,
|
||||
});
|
||||
links.push({ source: rootNodeId, target: floorNodeId });
|
||||
}
|
||||
|
||||
floors[floorId].areas.forEach((areaId) => {
|
||||
let targetNodeId: string;
|
||||
|
||||
if (areaId === "no_area" || !group_by_area) {
|
||||
targetNodeId = floorNodeId;
|
||||
} else {
|
||||
const areaNodeId = `area_${areaId}`;
|
||||
nodes.push({
|
||||
id: areaNodeId,
|
||||
label: this.hass.areas[areaId]?.name || areaId,
|
||||
value: areas[areaId].value,
|
||||
index: 3,
|
||||
color: primaryColor,
|
||||
});
|
||||
links.push({
|
||||
source: floorNodeId,
|
||||
target: areaNodeId,
|
||||
value: areas[areaId].value,
|
||||
});
|
||||
targetNodeId = areaNodeId;
|
||||
}
|
||||
|
||||
areas[areaId].devices.forEach((device) => {
|
||||
links.push({
|
||||
source: targetNodeId,
|
||||
target: device.id,
|
||||
value: device.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
devicesWithoutParent.forEach((deviceNode) => {
|
||||
links.push({
|
||||
source: rootNodeId,
|
||||
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 (only show if > 1 L/min threshold)
|
||||
if (untrackedConsumption > 1) {
|
||||
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: rootNodeId,
|
||||
target: "untracked",
|
||||
value: untrackedConsumption,
|
||||
});
|
||||
}
|
||||
|
||||
const hasData = nodes.some((node) => node.value > 0);
|
||||
|
||||
const vertical =
|
||||
layout === "vertical" || (layout !== "horizontal" && this._isMobileSize);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${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}
|
||||
@node-click=${this._handleNodeClick}
|
||||
></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;">
|
||||
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
|
||||
</div>`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
if (node.entityId) {
|
||||
fireEvent(this, "hass-more-info", { entityId: node.entityId });
|
||||
}
|
||||
}
|
||||
|
||||
private _getCurrentFlowRate(entityId: string): number {
|
||||
this._entities.add(entityId);
|
||||
return getFlowRateFromState(this.hass.states[entityId]) ?? 0;
|
||||
}
|
||||
|
||||
private _getEntityLabel(entityId: string): string {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) return entityId;
|
||||
return stateObj.attributes.friendly_name || entityId;
|
||||
}
|
||||
|
||||
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],
|
||||
};
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
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) {
|
||||
parentSection.push(deviceNode);
|
||||
} else {
|
||||
childSection.push(deviceNode);
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||
if (!parentSection.some((node) => node.id === parent)) {
|
||||
remainingLinks[child] = parent;
|
||||
}
|
||||
});
|
||||
|
||||
if (parentSection.length > 0) {
|
||||
return [
|
||||
parentSection,
|
||||
...this._getDeviceSections(remainingLinks, childSection),
|
||||
];
|
||||
}
|
||||
|
||||
return [deviceNodes];
|
||||
}
|
||||
|
||||
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-water-flow-sankey-card": HuiWaterFlowSankeyCard;
|
||||
}
|
||||
}
|
||||
@@ -67,13 +67,8 @@ const LAZY_LOAD_TYPES = {
|
||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||
"water-sankey": () => import("../cards/water/hui-water-sankey-card"),
|
||||
"water-flow-sankey": () =>
|
||||
import("../cards/water/hui-water-flow-sankey-card"),
|
||||
"power-sources-graph": () =>
|
||||
import("../cards/energy/hui-power-sources-graph-card"),
|
||||
"power-total": () => import("../cards/energy/hui-power-total-card"),
|
||||
"water-total": () => import("../cards/energy/hui-water-total-card"),
|
||||
"gas-total": () => import("../cards/energy/hui-gas-total-card"),
|
||||
"power-sankey": () => import("../cards/energy/hui-power-sankey-card"),
|
||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||
error: () => import("../cards/hui-error-card"),
|
||||
|
||||
@@ -142,6 +142,7 @@ export class HuiCreateDialogBadge
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
no-label-float
|
||||
.hass=${this.hass}
|
||||
.narrow=${true}
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
|
||||
@@ -127,30 +127,26 @@ export class HuiCreateDialogCard
|
||||
></ha-icon-button>
|
||||
<span slot="title">${title}</span>
|
||||
|
||||
${!this._params.saveCard
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "card"}
|
||||
panel="card"
|
||||
?autofocus=${this._narrow}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_card"
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing}
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "card"}
|
||||
panel="card"
|
||||
?autofocus=${this._narrow}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_card"
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
</ha-tab-group>
|
||||
</ha-dialog-header>
|
||||
${cache(
|
||||
this._currTab === "card"
|
||||
@@ -165,6 +161,7 @@ export class HuiCreateDialogCard
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
no-label-float
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
@@ -258,17 +255,6 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
}
|
||||
|
||||
if (this._params!.saveCard) {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveCardConfig: this._params!.saveCard,
|
||||
cardConfig: config,
|
||||
isNew: true,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
@@ -43,6 +43,9 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@property({ type: Array }) public entities?: string[];
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
@@ -112,6 +115,7 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.search"
|
||||
)}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.lovelace.unused_entities.no_data"
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
@@ -9,7 +8,6 @@ export interface CreateCardDialogParams {
|
||||
path: LovelaceContainerPath;
|
||||
suggestedCards?: string[];
|
||||
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
||||
saveCard?: (cardConfig: LovelaceCardConfig) => void; // Optional: pick a single card and return it via callback, hides entity tab
|
||||
}
|
||||
|
||||
export const importCreateCardDialog = () => import("./hui-dialog-create-card");
|
||||
|
||||
@@ -128,7 +128,52 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
${
|
||||
this._params.mode === "storage"
|
||||
? html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._saveConfig}
|
||||
.loading=${this._saving}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.save"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`
|
||||
: html`
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.yaml_mode"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.yaml_control"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.save_config.yaml_config"
|
||||
)}
|
||||
</p>
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._params!.lovelace.config}
|
||||
autofocus
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
${
|
||||
this._params.mode === "storage"
|
||||
? html`
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./hui-view-footer-settings-editor";
|
||||
import type { EditViewFooterDialogParams } from "./show-edit-view-footer-dialog";
|
||||
|
||||
@customElement("hui-dialog-edit-view-footer")
|
||||
export class HuiDialogEditViewFooter extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EditViewFooterDialogParams;
|
||||
|
||||
@state() private _config?: LovelaceViewFooterConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const config = {
|
||||
...this._config,
|
||||
};
|
||||
this._editor?.setValue(config);
|
||||
}
|
||||
}
|
||||
|
||||
public showDialog(params: EditViewFooterDialogParams): void {
|
||||
this._params = params;
|
||||
|
||||
this._dirty = false;
|
||||
this._config = this._params.config;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._config = undefined;
|
||||
this._yamlMode = false;
|
||||
this._dirty = false;
|
||||
this._saving = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let content: TemplateResult;
|
||||
|
||||
if (this._yamlMode) {
|
||||
content = html`
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
autofocus
|
||||
@value-changed=${this._viewYamlChanged}
|
||||
></ha-yaml-editor>
|
||||
`;
|
||||
} else {
|
||||
content = html`
|
||||
<hui-view-footer-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.maxColumns=${this._params.maxColumns}
|
||||
@config-changed=${this._configChanged}
|
||||
></hui-view-footer-settings-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
const title = this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_view_footer.header"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
.width=${this._yamlMode ? "full" : "large"}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${this._yamlMode ? "yaml-mode" : ""}
|
||||
>
|
||||
<ha-dropdown
|
||||
slot="headerActionItems"
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="toggle-mode">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${content}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._config || !this._dirty}
|
||||
@click=${this._save}
|
||||
.loading=${this._saving}
|
||||
>
|
||||
${this.hass!.localize("ui.common.save")}</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "toggle-mode") {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
if (
|
||||
ev.detail &&
|
||||
ev.detail.config &&
|
||||
!deepEqual(this._config, ev.detail.config)
|
||||
) {
|
||||
this._config = ev.detail.config;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _viewYamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
this._config = ev.detail.value;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._params || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._saving = true;
|
||||
|
||||
try {
|
||||
await this._params.saveConfig(this._config);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view_footer.saving_failed"
|
||||
)}: ${err.message}`,
|
||||
});
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog.yaml-mode {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-edit-view-footer": HuiDialogEditViewFooter;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("hui-view-footer-settings-editor")
|
||||
export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public maxColumns = 4;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = {
|
||||
column_span: this.config?.column_span || 1,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newData = ev.detail.value;
|
||||
|
||||
const config: LovelaceViewFooterConfig = {
|
||||
...this.config,
|
||||
...newData,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-footer-settings-editor": HuiViewFooterSettingsEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
|
||||
export interface EditViewFooterDialogParams {
|
||||
saveConfig: (config: LovelaceViewFooterConfig) => void;
|
||||
config: LovelaceViewFooterConfig;
|
||||
maxColumns: number;
|
||||
}
|
||||
|
||||
export const showEditViewFooterDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: EditViewFooterDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-edit-view-footer",
|
||||
dialogImport: () => import("./hui-dialog-edit-view-footer"),
|
||||
dialogParams: dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -239,15 +239,28 @@ export class LovelacePanel extends LitElement {
|
||||
|
||||
const newConfig = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
|
||||
|
||||
// Regenerate if the config changed
|
||||
// Ask to regenerate if the config changed
|
||||
if (!deepEqual(newConfig, oldConfig)) {
|
||||
this._regenerateStrategyConfig();
|
||||
this._askRegenerateStrategyConfig();
|
||||
}
|
||||
};
|
||||
|
||||
private _strategyConfigChanged = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
this._regenerateStrategyConfig();
|
||||
this._askRegenerateStrategyConfig();
|
||||
};
|
||||
|
||||
private _askRegenerateStrategyConfig = () => {
|
||||
showToast(this, {
|
||||
message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"),
|
||||
action: {
|
||||
action: () => this._regenerateStrategyConfig(),
|
||||
text: this.hass!.localize("ui.common.refresh"),
|
||||
},
|
||||
duration: -1,
|
||||
id: "regenerate-strategy-config",
|
||||
dismissable: false,
|
||||
});
|
||||
};
|
||||
|
||||
private async _regenerateStrategyConfig() {
|
||||
@@ -300,14 +313,8 @@ export class LovelacePanel extends LitElement {
|
||||
this._fetchConfigOnConnect = true;
|
||||
return;
|
||||
}
|
||||
if (!this.lovelace?.editMode && this._panelState !== "yaml-editor") {
|
||||
this._fetchConfig(false);
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass!.localize(
|
||||
"ui.panel.lovelace.externally_updated_toast.message"
|
||||
),
|
||||
message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"),
|
||||
action: {
|
||||
action: () => this._fetchConfig(false),
|
||||
text: this.hass!.localize("ui.common.refresh"),
|
||||
|
||||
@@ -7,20 +7,21 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { array, assert, object, optional, string, type } from "superstruct";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-code-editor";
|
||||
import type { HaCodeEditor } from "../../components/ha-code-editor";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
import "../../components/ha-button";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import type { Lovelace } from "./types";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
|
||||
const lovelaceStruct = type({
|
||||
title: optional(string()),
|
||||
@@ -112,7 +113,21 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
|
||||
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
|
||||
) {
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
showToast(this, {
|
||||
message: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.lovelace_changed"
|
||||
),
|
||||
action: {
|
||||
action: () => {
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
},
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.reload"
|
||||
),
|
||||
},
|
||||
duration: -1,
|
||||
dismissable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import type { Lovelace } from "../types";
|
||||
import { generateDefaultSection } from "./default-section";
|
||||
import "./hui-view-footer";
|
||||
import "./hui-view-header";
|
||||
import "./hui-view-sidebar";
|
||||
|
||||
@@ -309,12 +308,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<hui-view-footer
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.config=${this._config?.footer}
|
||||
></hui-view-footer>
|
||||
<div class="imported-cards-section">
|
||||
${editMode && this._config?.cards?.length
|
||||
? html`
|
||||
@@ -663,10 +656,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
padding-top: var(--row-gap);
|
||||
}
|
||||
|
||||
hui-view-footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.imported-cards {
|
||||
--column-span: var(--column-count);
|
||||
--row-span: 1;
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
import { mdiPencil, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewFooterConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardGridSize } from "../common/compute-card-grid-size";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceView } from "../editor/config-util";
|
||||
import { showEditViewFooterDialog } from "../editor/view-footer/show-edit-view-footer-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_MAX_COLUMNS } from "./hui-sections-view";
|
||||
|
||||
@customElement("hui-view-footer")
|
||||
export class HuiViewFooter extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public card?: HuiCard;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<typeof this>): void {
|
||||
if (changedProperties.has("config")) {
|
||||
if (this.config?.card) {
|
||||
this.card = this._createCardElement(this.config.card);
|
||||
} else {
|
||||
this.card = undefined;
|
||||
}
|
||||
this._checkHidden();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.card) {
|
||||
if (changedProperties.has("hass")) {
|
||||
this.card.hass = this.hass;
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this.card.preview = this.lovelace.editMode;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has("lovelace") || changedProperties.has("card")) {
|
||||
this._checkHidden();
|
||||
}
|
||||
}
|
||||
|
||||
private _checkHidden() {
|
||||
const hidden = !this.card && !this.lovelace?.editMode;
|
||||
this.toggleAttribute("hidden", hidden);
|
||||
this.toggleAttribute("sticky", Boolean(this.card));
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.lovelace.editMode;
|
||||
element.layout = "grid";
|
||||
element.config = cardConfig;
|
||||
element.addEventListener("card-updated", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this.requestUpdate();
|
||||
});
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
|
||||
private _addCard() {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.viewIndex],
|
||||
saveCard: (newCardConfig: LovelaceCardConfig) => {
|
||||
this._saveFooterConfig({ ...this.config, card: newCardConfig });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const newConfig = { ...this.config };
|
||||
delete newConfig.card;
|
||||
this._saveFooterConfig(newConfig);
|
||||
}
|
||||
|
||||
private _configure() {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
showEditViewFooterDialog(this, {
|
||||
config: this.config || {},
|
||||
maxColumns: viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
saveConfig: (newConfig: LovelaceViewFooterConfig) => {
|
||||
this._saveFooterConfig(newConfig);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const cardConfig = this.config?.card;
|
||||
if (!cardConfig) return;
|
||||
|
||||
showEditCardDialog(this, {
|
||||
cardConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: (newCardConfig: LovelaceCardConfig) => {
|
||||
this._saveFooterConfig({ ...this.config, card: newCardConfig });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _saveFooterConfig(footerConfig: LovelaceViewFooterConfig) {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
const config = { ...viewConfig, footer: footerConfig };
|
||||
|
||||
const updatedConfig = replaceView(
|
||||
this.hass,
|
||||
this.lovelace.config,
|
||||
this.viewIndex,
|
||||
config
|
||||
);
|
||||
this.lovelace.saveConfig(updatedConfig);
|
||||
}
|
||||
|
||||
private _renderCard(card: HuiCard, editMode: boolean) {
|
||||
const gridOptions = card.getGridOptions();
|
||||
const { rows } = computeCardGridSize(gridOptions);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="card ${classMap({
|
||||
"fit-rows": typeof rows === "number",
|
||||
})}"
|
||||
style=${styleMap({
|
||||
"--row-size": typeof rows === "number" ? String(rows) : undefined,
|
||||
})}
|
||||
>
|
||||
${editMode
|
||||
? html`
|
||||
<hui-card-edit-mode
|
||||
@ll-edit-card=${this._editCard}
|
||||
@ll-delete-card=${this._deleteCard}
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace!}
|
||||
.path=${[0]}
|
||||
no-duplicate
|
||||
no-move
|
||||
>
|
||||
${card}
|
||||
</hui-card-edit-mode>
|
||||
`
|
||||
: card}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
const editMode = Boolean(this.lovelace?.editMode);
|
||||
const card = this.card;
|
||||
|
||||
if (!card && !editMode) return nothing;
|
||||
|
||||
const columnSpan = this.config?.column_span || 1;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({ wrapper: true, "edit-mode": editMode })}
|
||||
style=${styleMap({
|
||||
"--footer-column-span": String(columnSpan),
|
||||
})}
|
||||
>
|
||||
${editMode
|
||||
? html`
|
||||
<div class="actions-container">
|
||||
<div class="actions">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
@click=${this._configure}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class=${classMap({ container: true, "edit-mode": editMode })}>
|
||||
${card
|
||||
? this._renderCard(card, editMode)
|
||||
: editMode
|
||||
? html`
|
||||
<button class="add" @click=${this._addCard}>
|
||||
<ha-ripple></ha-ripple>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_view_footer.add"
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host([hidden]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:host([sticky]) {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: var(--ha-space-4) 0;
|
||||
padding-bottom: max(
|
||||
var(--ha-space-4),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
max-width: calc(
|
||||
var(--footer-column-span, 1) * var(--column-max-width, 500px) +
|
||||
(var(--footer-column-span, 1) - 1) * var(--column-gap, 32px)
|
||||
);
|
||||
}
|
||||
|
||||
.wrapper:not(.edit-mode) {
|
||||
--ha-card-box-shadow:
|
||||
0px 3px 5px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 6px 10px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 18px 0px rgba(0, 0, 0, 0.12);
|
||||
--ha-card-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
--row-height: var(--ha-section-grid-row-height, 56px);
|
||||
--row-gap: var(--ha-section-grid-row-gap, 8px);
|
||||
--column-gap: var(--ha-section-grid-column-gap, 8px);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
grid-auto-rows: auto;
|
||||
row-gap: var(--row-gap);
|
||||
column-gap: var(--column-gap);
|
||||
}
|
||||
|
||||
.container.edit-mode {
|
||||
padding: var(--ha-space-2);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
border: 2px dashed var(--divider-color);
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
grid-column: 1 / -1;
|
||||
grid-row: span var(--row-size, 1);
|
||||
max-height: 25vh;
|
||||
max-height: 25dvh;
|
||||
}
|
||||
|
||||
.card.fit-rows {
|
||||
height: calc(
|
||||
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
|
||||
--row-gap
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
height: 36px;
|
||||
bottom: -2px;
|
||||
right: 0;
|
||||
inset-inline-end: 0;
|
||||
inset-inline-start: initial;
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
background: var(--secondary-background-color);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.add {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
gap: var(--ha-space-2);
|
||||
height: 36px;
|
||||
padding: 6px 20px 6px 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
background-color: transparent;
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: var(--primary-color);
|
||||
--mdc-icon-size: 18px;
|
||||
cursor: pointer;
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--primary-text-color);
|
||||
--ha-ripple-color: var(--primary-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
}
|
||||
|
||||
.add:focus {
|
||||
border-style: solid;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-footer": HuiViewFooter;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export const securityEntityFilters: EntityFilter[] = [
|
||||
},
|
||||
{
|
||||
domain: "cover",
|
||||
device_class: ["door", "garage", "gate", "window"],
|
||||
device_class: ["door", "garage", "gate"],
|
||||
entity_category: "none",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -186,10 +186,24 @@ export const haStyleDialog = css`
|
||||
var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)
|
||||
var(--safe-area-inset-left, 0);
|
||||
--vertical-align-dialog: flex-end;
|
||||
}
|
||||
ha-dialog {
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
ha-dialog,
|
||||
ha-adaptive-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100vh;
|
||||
--mdc-dialog-min-height: 100svh;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-max-height: 100svh;
|
||||
--dialog-container-padding: 0px;
|
||||
--dialog-surface-padding: var(--safe-area-inset-top, 0)
|
||||
var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)
|
||||
var(--safe-area-inset-left, 0);
|
||||
--vertical-align-dialog: flex-end;
|
||||
}
|
||||
ha-dialog {
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
|
||||
@@ -4,7 +4,7 @@ export const waColorStyles = css`
|
||||
html {
|
||||
--wa-color-brand-fill-loud: var(--ha-color-fill-primary-loud-resting);
|
||||
--wa-color-brand-fill-normal: var(--ha-color-fill-primary-normal-resting);
|
||||
--wa-color-brand-fill-quiet: var(--ha-color-fill-primary-quiet-hover);
|
||||
--wa-color-brand-fill-quiet: var(--ha-color-fill-primary-quiet-resting);
|
||||
--wa-color-brand-border-loud: var(--ha-color-border-loud);
|
||||
--wa-color-brand-border-normal: var(--ha-color-primary-50);
|
||||
--wa-color-brand-border-quiet: var(--ha-color-border-quiet);
|
||||
@@ -14,7 +14,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-neutral-fill-loud: var(--ha-color-fill-neutral-loud-resting);
|
||||
--wa-color-neutral-fill-normal: var(--ha-color-fill-neutral-normal-resting);
|
||||
--wa-color-neutral-fill-quiet: var(--ha-color-fill-neutral-quiet-hover);
|
||||
--wa-color-neutral-fill-quiet: var(--ha-color-fill-neutral-quiet-resting);
|
||||
--wa-color-neutral-border-loud: var(--ha-color-border-neutral-loud);
|
||||
--wa-color-neutral-border-normal: var(--ha-color-border-neutral-normal);
|
||||
--wa-color-neutral-border-quiet: var(--ha-color-border-neutral-quiet);
|
||||
@@ -24,7 +24,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-success-fill-loud: var(--ha-color-fill-success-loud-resting);
|
||||
--wa-color-success-fill-normal: var(--ha-color-fill-success-normal-resting);
|
||||
--wa-color-success-fill-quiet: var(--ha-color-fill-success-quiet-hover);
|
||||
--wa-color-success-fill-quiet: var(--ha-color-fill-success-quiet-resting);
|
||||
--wa-color-success-border-loud: var(--ha-color-border-success-loud);
|
||||
--wa-color-success-border-normal: var(--ha-color-border-success-normal);
|
||||
--wa-color-success-border-quiet: var(--ha-color-border-success-quiet);
|
||||
@@ -34,7 +34,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-warning-fill-loud: var(--ha-color-fill-warning-loud-resting);
|
||||
--wa-color-warning-fill-normal: var(--ha-color-fill-warning-normal-resting);
|
||||
--wa-color-warning-fill-quiet: var(--ha-color-fill-warning-quiet-hover);
|
||||
--wa-color-warning-fill-quiet: var(--ha-color-fill-warning-quiet-resting);
|
||||
--wa-color-warning-border-loud: var(--ha-color-border-warning-loud);
|
||||
--wa-color-warning-border-normal: var(--ha-color-border-warning-normal);
|
||||
--wa-color-warning-border-quiet: var(--ha-color-border-warning-quiet);
|
||||
@@ -44,7 +44,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-color-danger-fill-loud: var(--ha-color-fill-danger-loud-resting);
|
||||
--wa-color-danger-fill-normal: var(--ha-color-fill-danger-normal-resting);
|
||||
--wa-color-danger-fill-quiet: var(--ha-color-fill-danger-quiet-hover);
|
||||
--wa-color-danger-fill-quiet: var(--ha-color-fill-danger-quiet-resting);
|
||||
--wa-color-danger-border-loud: var(--ha-color-border-danger-loud);
|
||||
--wa-color-danger-border-normal: var(--ha-color-border-danger-normal);
|
||||
--wa-color-danger-border-quiet: var(--ha-color-border-danger-quiet);
|
||||
@@ -64,10 +64,5 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||
|
||||
--wa-form-control-background-color: var(--wa-color-surface-raised);
|
||||
--wa-form-control-border-color: var(--ha-color-border-neutral-quiet);
|
||||
--wa-form-control-value-color: var(--primary-text-color);
|
||||
--wa-form-control-placeholder-color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -14,8 +14,10 @@ export const waMainStyles = css`
|
||||
--wa-space-l: var(--ha-space-6);
|
||||
--wa-space-xl: var(--ha-space-8);
|
||||
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-font-weight-body: var(--ha-font-weight-normal);
|
||||
--wa-transition-normal: 150ms;
|
||||
--wa-transition-fast: 75ms;
|
||||
--wa-transition-easing: ease;
|
||||
@@ -27,25 +29,13 @@ export const waMainStyles = css`
|
||||
--wa-border-radius-s: var(--ha-border-radius-sm);
|
||||
--wa-border-radius-m: var(--ha-border-radius-md);
|
||||
--wa-border-radius-l: var(--ha-border-radius-lg);
|
||||
--wa-border-radius-pill: var(--ha-border-radius-pill);
|
||||
|
||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-size-s: var(--ha-font-size-s);
|
||||
--wa-font-size-m: var(--ha-font-size-m);
|
||||
--wa-font-size-l: var(--ha-font-size-l);
|
||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
||||
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
--wa-form-control-value-line-height: var(--wa-line-height-condensed);
|
||||
--wa-form-control-value-font-weight: var(--wa-font-weight-body);
|
||||
--wa-form-control-border-radius: var(--wa-border-radius-l);
|
||||
--wa-form-control-border-style: var(--wa-border-style);
|
||||
--wa-form-control-border-width: var(--wa-border-width-s);
|
||||
--wa-form-control-height: 40px;
|
||||
--wa-form-control-padding-inline: var(--ha-space-3);
|
||||
}
|
||||
|
||||
${scrollLockStyles}
|
||||
|
||||
@@ -30,10 +30,6 @@ import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_displ
|
||||
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import {
|
||||
clearBrandsTokenRefresh,
|
||||
fetchAndScheduleBrandsAccessToken,
|
||||
} from "../util/brands-url";
|
||||
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import { fetchWithAuth } from "../util/fetch-with-auth";
|
||||
@@ -323,10 +319,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ systemData: {} });
|
||||
});
|
||||
clearInterval(this.__backendPingInterval);
|
||||
|
||||
// Fetch the brands access token on initial connect and schedule refresh
|
||||
fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
// If the backend is busy, or the connection is latent,
|
||||
@@ -351,9 +343,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ connected: true });
|
||||
broadcastConnectionStatus("connected");
|
||||
|
||||
// Refresh the brands access token on reconnect and restart refresh schedule
|
||||
fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
|
||||
// on reconnect always fetch config as we might miss an update while we were disconnected
|
||||
// @ts-ignore
|
||||
this.hass!.callWS({ type: "get_config" }).then((config: HassConfig) => {
|
||||
@@ -371,6 +360,5 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({ connected: false });
|
||||
broadcastConnectionStatus("disconnected");
|
||||
clearInterval(this.__backendPingInterval);
|
||||
clearBrandsTokenRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ContextProvider } from "@lit/context";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
areasContext,
|
||||
authContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
devicesContext,
|
||||
@@ -101,6 +102,10 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
context: labelsContext,
|
||||
initialValue: [],
|
||||
}),
|
||||
auth: new ContextProvider(this, {
|
||||
context: authContext,
|
||||
initialValue: this.hass?.auth,
|
||||
}),
|
||||
};
|
||||
|
||||
protected hassConnected() {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this.addEventListener("register-dialog", (e) =>
|
||||
this.registerDialog(e.detail)
|
||||
);
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
makeDialogManager(this);
|
||||
}
|
||||
|
||||
protected registerDialog({
|
||||
@@ -44,10 +44,10 @@ export const dialogManagerMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this.addEventListener(dialogShowEvent, (showEv) => {
|
||||
showDialog(
|
||||
this,
|
||||
this.shadowRoot!,
|
||||
dialogTag,
|
||||
(showEv as HASSDomEvent<unknown>).detail,
|
||||
dialogImport,
|
||||
undefined,
|
||||
addHistory
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user