mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-29 12:31:52 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8894c357ec | |||
| 479b25532b | |||
| ac120ad309 | |||
| 5a2e4b0da5 | |||
| 9e56fd379e | |||
| 109d21aa81 | |||
| aacb8e3c09 | |||
| 6def14718d | |||
| fb0a54231a | |||
| a147fc4fee | |||
| a300085208 | |||
| 44989a6972 | |||
| 54a8e6c294 | |||
| bfec22d828 | |||
| cde6450cfc | |||
| ab39e70629 | |||
| 69f209e3c3 | |||
| f4c5561a54 | |||
| 5147937a6f | |||
| ee39605aa7 | |||
| 4af4f1dc51 | |||
| a2d8859d94 | |||
| afea8180c4 | |||
| b9c077489d | |||
| 440bb32056 | |||
| 8f371621ad | |||
| 61815b20e3 | |||
| 1942fa3a77 | |||
| 865e67a06f | |||
| 412dce4c1f | |||
| ced2ac7ad5 | |||
| 6649f52bcd | |||
| 7dbd6ae5a2 | |||
| e1528d21b3 | |||
| 79cb3137f2 | |||
| 313360701a | |||
| b100d9577d | |||
| 44ce303302 | |||
| 8f76613068 | |||
| 85dff6640a | |||
| ab7c892b6b | |||
| 3fe57ad724 | |||
| 1caf1d99b5 | |||
| 483df2fa2f | |||
| e0adb006b6 | |||
| 50e34015b3 | |||
| c1c926c631 | |||
| c41afac57c | |||
| 8856c26929 | |||
| 4a0fe3190c | |||
| 08f7e97462 | |||
| a5791c8c08 | |||
| 6a98a74c58 | |||
| c1df3bc38e |
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
|
||||
+248
-248
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.14.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.15.0.cjs
|
||||
|
||||
+25
-25
@@ -38,24 +38,24 @@
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.5",
|
||||
"@formatjs/intl-displaynames": "7.3.7",
|
||||
"@formatjs/intl-durationformat": "0.10.11",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.8",
|
||||
"@formatjs/intl-listformat": "8.3.7",
|
||||
"@formatjs/intl-locale": "5.3.7",
|
||||
"@formatjs/intl-numberformat": "9.3.8",
|
||||
"@formatjs/intl-pluralrules": "6.3.7",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.7",
|
||||
"@formatjs/intl-datetimeformat": "7.4.6",
|
||||
"@formatjs/intl-displaynames": "7.3.8",
|
||||
"@formatjs/intl-durationformat": "0.10.12",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.9",
|
||||
"@formatjs/intl-listformat": "8.3.8",
|
||||
"@formatjs/intl-locale": "5.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.9",
|
||||
"@formatjs/intl-pluralrules": "6.3.8",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.8",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.3.1-ha.3",
|
||||
"@home-assistant/webawesome": "3.7.0-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -74,8 +74,8 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.0.2",
|
||||
"@tsparticles/preset-links": "4.0.2",
|
||||
"@tsparticles/engine": "4.0.5",
|
||||
"@tsparticles/preset-links": "4.0.5",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -86,7 +86,7 @@
|
||||
"core-js": "3.49.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.2.1",
|
||||
"date-fns": "4.3.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
@@ -97,8 +97,8 @@
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.6",
|
||||
"idb-keyval": "6.2.4",
|
||||
"intl-messageformat": "11.2.7",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -106,7 +106,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "18.0.3",
|
||||
"marked": "18.0.4",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -118,7 +118,7 @@
|
||||
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"tinykeys": "4.0.0",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
"workbox-core": "7.4.1",
|
||||
@@ -141,7 +141,7 @@
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.11",
|
||||
"@rspack/core": "2.0.3",
|
||||
"@rspack/core": "2.0.4",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -160,7 +160,7 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.6",
|
||||
"@vitest/coverage-v8": "4.1.7",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
@@ -175,7 +175,7 @@
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.5",
|
||||
"generate-license-file": "4.1.1",
|
||||
"generate-license-file": "4.2.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"gulp": "5.0.1",
|
||||
@@ -201,9 +201,9 @@
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.3",
|
||||
"typescript-eslint": "8.59.4",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.6",
|
||||
"vitest": "4.1.7",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
@@ -219,8 +219,8 @@
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.14.1",
|
||||
"packageManager": "yarn@4.15.0",
|
||||
"volta": {
|
||||
"node": "24.15.0"
|
||||
"node": "24.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import timezones from "google-timezones-json";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
|
||||
const RESOLVED_TIME_ZONE = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
|
||||
// Some environments (e.g. Android emulator) return a UTC offset like "+00:00"
|
||||
// instead of an IANA zone name. Only accept values that are known IANA zones,
|
||||
// matching the list used by ha-timezone-picker.
|
||||
const RESOLVED_TIME_ZONE =
|
||||
RESOLVED_RAW &&
|
||||
(RESOLVED_RAW === "UTC" ||
|
||||
RESOLVED_RAW === "Etc/UTC" ||
|
||||
RESOLVED_RAW in timezones)
|
||||
? RESOLVED_RAW
|
||||
: undefined;
|
||||
|
||||
export const HAS_RESOLVED_IANA_TIME_ZONE = RESOLVED_TIME_ZONE !== undefined;
|
||||
|
||||
// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support.
|
||||
export const LOCAL_TIME_ZONE = RESOLVED_TIME_ZONE ?? "UTC";
|
||||
|
||||
@@ -128,7 +128,9 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0 0 0 var(--ha-space-3);
|
||||
padding-left: var(--ha-space-3);
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
padding-inline-end: initial;
|
||||
min-height: 48px;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
@@ -144,6 +146,8 @@ export class HaAutomationRow extends LitElement {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
margin-left: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-2) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
:host([building-block]) .leading-icon-wrapper {
|
||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
|
||||
@@ -294,6 +294,7 @@ export class HaDrawer extends LitElement {
|
||||
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
box-sizing: border-box;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
|
||||
@@ -109,6 +109,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@property({ attribute: "custom-value-label" })
|
||||
public customValueLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -271,6 +273,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.selectedSection=${this.selectedSection}
|
||||
.searchKeys=${this.searchKeys}
|
||||
.customValueLabel=${this.customValueLabel}
|
||||
.noSort=${this.noSort}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
@@ -342,7 +344,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _getItems = () => {
|
||||
let items = [...(this.getItems(this._search, this._selectedSection) || [])];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
if (!this.sections?.length && !this.noSort) {
|
||||
items = items.sort((entityA, entityB) => {
|
||||
const sortLabelA =
|
||||
typeof entityA === "string" ? entityA : entityA.sorting_label;
|
||||
|
||||
@@ -199,6 +199,7 @@ export class HaSelectSelector extends LitElement {
|
||||
: nothing}
|
||||
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -215,6 +216,7 @@ export class HaSelectSelector extends LitElement {
|
||||
if (this.selector.select?.custom_value) {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
no-sort
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
const SEARCH_KEYS = [{ name: "primary", weight: 1 }];
|
||||
|
||||
@customElement("ha-theme-picker")
|
||||
export class HaThemePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@@ -25,52 +29,74 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "no-theme-label" }) public noThemeLabel?: string;
|
||||
|
||||
private _getThemeOptions = memoizeOne(
|
||||
(
|
||||
themes: Record<string, unknown>,
|
||||
locale: string,
|
||||
includeDefault: boolean
|
||||
): PickerComboBoxItem[] => {
|
||||
const items: PickerComboBoxItem[] = [];
|
||||
|
||||
if (includeDefault) {
|
||||
items.push({ id: DEFAULT_THEME, primary: "Home Assistant" });
|
||||
}
|
||||
|
||||
const themeNames = Object.keys(themes).sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a, b, locale)
|
||||
);
|
||||
for (const theme of themeNames) {
|
||||
items.push({ id: theme, primary: theme });
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getThemeOptions(
|
||||
this.hass?.themes.themes || {},
|
||||
this.hass?.locale.language || "en",
|
||||
this.includeDefault
|
||||
);
|
||||
|
||||
private _valueRenderer = (value: string): TemplateResult =>
|
||||
html`<span slot="headline"
|
||||
>${this._getItems().find((i) => i.id === value)?.primary ?? value}</span
|
||||
>`;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options: HaSelectOption[] = Object.keys(
|
||||
this.hass?.themes.themes || {}
|
||||
).map((theme) => ({
|
||||
value: theme,
|
||||
}));
|
||||
|
||||
if (this.includeDefault) {
|
||||
options.unshift({
|
||||
value: DEFAULT_THEME,
|
||||
label: "Home Assistant",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: "remove",
|
||||
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.theme-picker.theme")}
|
||||
.value=${this.value}
|
||||
<ha-generic-picker
|
||||
.label=${this.label ??
|
||||
this.hass?.localize("ui.components.theme-picker.theme") ??
|
||||
"Theme"}
|
||||
.placeholder=${this.noThemeLabel ??
|
||||
this.hass?.localize("ui.components.theme-picker.no_theme")}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
private _changed(ev: ValueChangedEvent<string | undefined>): void {
|
||||
ev.stopPropagation();
|
||||
this.value = ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ export class HaTileContainer extends LitElement {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.vertical ::slotted([slot="info"]) {
|
||||
width: 100%;
|
||||
|
||||
@@ -615,7 +615,6 @@ export interface BaseSidebarConfig {
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Trigger) => void;
|
||||
editId: () => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
duplicate: () => void;
|
||||
|
||||
@@ -125,15 +125,20 @@ export const getTriggerInfos = (
|
||||
}
|
||||
const map = new Map<string, TriggerInfo>();
|
||||
for (const t of flattenTriggers(triggers)) {
|
||||
if (isTriggerList(t) || !t.id || map.get(t.id)) {
|
||||
if (isTriggerList(t) || !t.id) {
|
||||
continue;
|
||||
}
|
||||
map.set(t.id, {
|
||||
id: t.id,
|
||||
label: describeTrigger(t, hass, entityRegistry),
|
||||
triggerType: t.trigger,
|
||||
count: 1,
|
||||
});
|
||||
const existing = map.get(t.id);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
map.set(t.id, {
|
||||
id: t.id,
|
||||
label: describeTrigger(t, hass, entityRegistry),
|
||||
triggerType: t.trigger,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
+11
-2
@@ -17,6 +17,7 @@ export interface BluetoothDeviceData extends DataTableRowData {
|
||||
source: string;
|
||||
time: number;
|
||||
tx_power: number;
|
||||
raw: string | null;
|
||||
}
|
||||
|
||||
export interface BluetoothConnectionData extends DataTableRowData {
|
||||
@@ -58,13 +59,21 @@ export interface BluetoothAllocationsData {
|
||||
allocated: string[];
|
||||
}
|
||||
|
||||
export type BluetoothScannerMode = "active" | "passive";
|
||||
|
||||
export type BluetoothScannerRequestedMode = BluetoothScannerMode | "auto";
|
||||
|
||||
export interface BluetoothScannerState {
|
||||
source: string;
|
||||
adapter: string;
|
||||
current_mode: "active" | "passive" | null;
|
||||
requested_mode: "active" | "passive" | null;
|
||||
current_mode: BluetoothScannerMode | null;
|
||||
requested_mode: BluetoothScannerRequestedMode | null;
|
||||
}
|
||||
|
||||
export const isScannerStateMismatch = (state: BluetoothScannerState): boolean =>
|
||||
state.requested_mode !== "auto" &&
|
||||
state.current_mode !== state.requested_mode;
|
||||
|
||||
export const subscribeBluetoothScannersDetailsUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<BluetoothScannersDetails>
|
||||
|
||||
@@ -5,7 +5,10 @@ export interface DataTableFilter {
|
||||
|
||||
export type DataTableFilters = Record<string, DataTableFilter>;
|
||||
|
||||
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
|
||||
export type DataTableFiltersValue =
|
||||
| string[]
|
||||
| Record<"key" | string, string[]>
|
||||
| undefined;
|
||||
|
||||
export type DataTableFiltersValues = Record<string, DataTableFiltersValue>;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
mdiPlaylistMusic,
|
||||
mdiPlayPause,
|
||||
mdiPodcast,
|
||||
mdiPower,
|
||||
mdiPowerStandby,
|
||||
mdiPowerOff,
|
||||
mdiPowerOn,
|
||||
mdiRepeat,
|
||||
@@ -295,7 +295,7 @@ export const computeMediaControls = (
|
||||
return supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
|
||||
? [
|
||||
{
|
||||
icon: mdiPower,
|
||||
icon: mdiPowerStandby,
|
||||
action: "turn_on",
|
||||
},
|
||||
]
|
||||
@@ -316,7 +316,7 @@ export const computeMediaControls = (
|
||||
|
||||
if (supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)) {
|
||||
buttons.push({
|
||||
icon: assumedState ? mdiPowerOff : mdiPower,
|
||||
icon: assumedState ? mdiPowerOff : mdiPowerStandby,
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +62,44 @@ export const getTriggerIds = (triggers: Trigger[]): string[] =>
|
||||
.map((trigger) => trigger.id)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
export const getNextNumericTriggerId = (triggers: Trigger[]): string => {
|
||||
let max = 0;
|
||||
for (const id of getTriggerIds(triggers)) {
|
||||
const num = Number(id);
|
||||
if (Number.isInteger(num) && num > max) {
|
||||
max = num;
|
||||
}
|
||||
}
|
||||
return String(max + 1);
|
||||
};
|
||||
|
||||
const computeUniqueId = (id: string, existing: Set<string>): string => {
|
||||
if (!existing.has(id)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Split into a base and a trailing integer suffix so we can bump the
|
||||
// suffix on collision (e.g. "foo2" -> "foo3"); if there's no trailing
|
||||
// digit we start at 2 ("foo" -> "foo2").
|
||||
const match = id.match(/^(.*?)(\d+)$/);
|
||||
let base: string;
|
||||
let num: number;
|
||||
if (match) {
|
||||
base = match[1];
|
||||
num = Number(match[2]) + 1;
|
||||
} else {
|
||||
base = id;
|
||||
num = 2;
|
||||
}
|
||||
while (existing.has(`${base}${num}`)) {
|
||||
num++;
|
||||
}
|
||||
return `${base}${num}`;
|
||||
};
|
||||
|
||||
export const getUniqueTriggerId = (id: string, triggers: Trigger[]): string =>
|
||||
computeUniqueId(id, new Set(getTriggerIds(triggers)));
|
||||
|
||||
export interface TriggerDescription {
|
||||
target?: TargetSelector["target"];
|
||||
fields: Record<
|
||||
|
||||
@@ -87,6 +87,19 @@ const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
|
||||
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
|
||||
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
|
||||
|
||||
// The hassio integration sets these as hard-coded `_attr_title` on the Core,
|
||||
// Operating System, and Supervisor update entities. They are not translated,
|
||||
// so a title comparison is the reliable way to identify them without depending
|
||||
// on the (lazily-fetched) entity sources.
|
||||
export const isSystemUpdate = (entity: UpdateEntity): boolean => {
|
||||
const title = entity.attributes.title || "";
|
||||
return (
|
||||
title === HOME_ASSISTANT_CORE_TITLE ||
|
||||
title === HOME_ASSISTANT_OS_TITLE ||
|
||||
title === HOME_ASSISTANT_SUPERVISOR_TITLE
|
||||
);
|
||||
};
|
||||
|
||||
export const filterUpdateEntities = (
|
||||
entities: HassEntities,
|
||||
language?: string
|
||||
@@ -133,6 +146,11 @@ export const filterUpdateEntitiesParameterized = (
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
|
||||
export const installUpdates = (hass: HomeAssistant, entityIds: string[]) =>
|
||||
hass.callService("update", "install", {
|
||||
entity_id: entityIds,
|
||||
});
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { createSearchParam } from "../../common/url/search-params";
|
||||
import type { SingleHassServiceTarget } from "../../data/target";
|
||||
import {
|
||||
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
@@ -105,6 +106,8 @@ export function addToActionHandler(
|
||||
searchParams[ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM] = target.entity_id;
|
||||
} else if (target.device_id) {
|
||||
searchParams[ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM] = target.device_id;
|
||||
} else if (target.area_id) {
|
||||
searchParams[ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM] = target.area_id;
|
||||
}
|
||||
|
||||
const params = (addElement: string) =>
|
||||
|
||||
@@ -343,9 +343,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
this._step = this._previousSteps.pop()!;
|
||||
}
|
||||
|
||||
private _goToNextStep(ev?: CustomEvent) {
|
||||
private async _goToNextStep(ev?: CustomEvent) {
|
||||
if (ev?.detail?.updateConfig) {
|
||||
this._fetchAssistConfiguration();
|
||||
await this._fetchAssistConfiguration();
|
||||
}
|
||||
if (ev?.detail?.nextStep) {
|
||||
this._nextStep = ev.detail.nextStep;
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
HAS_RESOLVED_IANA_TIME_ZONE,
|
||||
LOCAL_TIME_ZONE,
|
||||
} from "../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import { COUNTRIES } from "../components/ha-country-picker";
|
||||
import "../components/ha-form/ha-form";
|
||||
import type { HaForm } from "../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../components/ha-form/types";
|
||||
import "../components/ha-spinner";
|
||||
import type { ConfigUpdateValues } from "../data/core";
|
||||
import { saveCoreConfig } from "../data/core";
|
||||
import { countryCurrency } from "../data/currency";
|
||||
import { onboardCoreConfigStep } from "../data/onboarding";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { getLocalLanguage } from "../util/common-translation";
|
||||
import "./onboarding-location";
|
||||
|
||||
@@ -28,7 +35,9 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
private _elevation = "0";
|
||||
|
||||
private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE;
|
||||
@state() private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE;
|
||||
|
||||
@state() private _timeZoneDetected = HAS_RESOLVED_IANA_TIME_ZONE;
|
||||
|
||||
private _language: ConfigUpdateValues["language"] = getLocalLanguage();
|
||||
|
||||
@@ -42,7 +51,29 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
@state() private _skipCore = false;
|
||||
|
||||
@query("ha-country-picker") private _countryPicker?: HTMLElement;
|
||||
@query("ha-form") private _form?: HaForm;
|
||||
|
||||
private _schema = memoizeOne((includeTimeZone: boolean): HaFormSchema[] => [
|
||||
{
|
||||
name: "country",
|
||||
required: true,
|
||||
selector: { country: null },
|
||||
},
|
||||
...(includeTimeZone
|
||||
? ([
|
||||
{
|
||||
name: "time_zone",
|
||||
required: true,
|
||||
selector: { timezone: null },
|
||||
},
|
||||
] satisfies HaFormSchema[])
|
||||
: []),
|
||||
]);
|
||||
|
||||
private _computeLabel = (schema: HaFormSchema) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.core.section.core.core_config.${schema.name}` as any
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._location) {
|
||||
@@ -68,17 +99,17 @@ class OnboardingCoreConfig extends LitElement {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<ha-country-picker
|
||||
class="flex"
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.country"
|
||||
) || "Country"}
|
||||
required
|
||||
.data=${{
|
||||
country: this._country ?? "",
|
||||
time_zone: this._timeZone,
|
||||
}}
|
||||
.schema=${this._schema(!this._timeZoneDetected)}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.disabled=${this._working}
|
||||
.value=${this._countryValue}
|
||||
@value-changed=${this._handleCountryChanged}
|
||||
></ha-country-picker>
|
||||
@value-changed=${this._handleFormChanged}
|
||||
></ha-form>
|
||||
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._save} .disabled=${this._working}>
|
||||
@@ -99,12 +130,12 @@ class OnboardingCoreConfig extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private get _countryValue() {
|
||||
return this._country || "";
|
||||
}
|
||||
|
||||
private _handleCountryChanged(ev: ValueChangedEvent<string>) {
|
||||
this._country = ev.detail.value;
|
||||
private _handleFormChanged(ev: CustomEvent) {
|
||||
const value = ev.detail.value as { country?: string; time_zone?: string };
|
||||
this._country = value.country || undefined;
|
||||
if (value.time_zone) {
|
||||
this._timeZone = value.time_zone;
|
||||
}
|
||||
}
|
||||
|
||||
private async _locationChanged(ev) {
|
||||
@@ -123,11 +154,12 @@ class OnboardingCoreConfig extends LitElement {
|
||||
}
|
||||
if (ev.detail.value.timezone) {
|
||||
this._timeZone = ev.detail.value.timezone;
|
||||
this._timeZoneDetected = true;
|
||||
}
|
||||
if (ev.detail.value.unit_system) {
|
||||
this._unitSystem = ev.detail.value.unit_system;
|
||||
}
|
||||
if (this._country) {
|
||||
if (this._country && this._timeZoneDetected) {
|
||||
this._skipCore = true;
|
||||
this._save(ev);
|
||||
return;
|
||||
@@ -145,11 +177,11 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
fireEvent(this, "onboarding-progress", { increase: 0.5 });
|
||||
await this.updateComplete;
|
||||
setTimeout(() => this._countryPicker!.focus(), 100);
|
||||
setTimeout(() => this._form?.focus(), 100);
|
||||
}
|
||||
|
||||
private async _save(ev) {
|
||||
if (!this._location || !this._country) {
|
||||
if (!this._location || !this._country || !this._timeZone) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
@@ -166,7 +198,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
this._unitSystem || ["US", "MM", "LR"].includes(this._country)
|
||||
? "us_customary"
|
||||
: "metric",
|
||||
time_zone: this._timeZone || "UTC",
|
||||
time_zone: this._timeZone,
|
||||
currency: this._currency || countryCurrency[this._country] || "EUR",
|
||||
country: this._country,
|
||||
language: this._language,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { IFRAME_SANDBOX } from "../../util/iframe";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeRouteTail } from "../../common/url/route";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
@@ -136,6 +137,8 @@ class HaPanelApp extends LitElement {
|
||||
})}
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
.sandbox=${IFRAME_SANDBOX}
|
||||
allow="microphone; camera; clipboard-write"
|
||||
@load=${this._checkLoaded}
|
||||
${ref(this._iframeRef)}
|
||||
>
|
||||
|
||||
@@ -190,16 +190,20 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
@click=${this._handleRefresh}
|
||||
></ha-icon-button>
|
||||
${showPane && this.hass.user?.is_admin
|
||||
? html`<ha-list slot="pane" multi}>${calendarItems}</ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
slot="pane-footer"
|
||||
@click=${this._addCalendar}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="graphic"></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.calendar.create_calendar")}
|
||||
</ha-list-item>`
|
||||
${showPane
|
||||
? html`<ha-list slot="pane" multi>${calendarItems}</ha-list>${this
|
||||
.hass.user?.is_admin
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
slot="pane-footer"
|
||||
@click=${this._addCalendar}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus} slot="graphic"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.create_calendar"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
: nothing}`
|
||||
: nothing}
|
||||
<ha-full-calendar
|
||||
add-fab
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
mdiPalette,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistCheck,
|
||||
mdiRobotOutline,
|
||||
mdiScriptTextOutline,
|
||||
} from "@mdi/js";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-adaptive-dialog";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
areasContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import type { SceneEntities } from "../../../data/scene";
|
||||
import { showSceneEditor } from "../../../data/scene";
|
||||
import {
|
||||
addToActionHandler,
|
||||
type AddToActionKey,
|
||||
} from "../../../dialogs/more-info/add-to";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { AreaAddToDialogParams } from "./show-dialog-area-add-to";
|
||||
|
||||
@customElement("dialog-area-add-to")
|
||||
class DialogAreaAddTo extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@state() private _params?: AreaAddToDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: AreaAddToDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._renderOptions()}
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOptions() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const area = this._areas[this._params.areaId];
|
||||
const areaName = computeAreaName(area) || this._params.areaId;
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"automation_trigger",
|
||||
mdiRobotOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_condition",
|
||||
mdiPlaylistCheck,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_action",
|
||||
mdiPlayCircleOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"script_action",
|
||||
mdiScriptTextOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
${this._renderSceneSection(areaName)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSceneSection(areaName: string) {
|
||||
if (!this._params?.entityIds.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._handleCreateScene}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.scene",
|
||||
{ target: areaName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionItem(
|
||||
key: AddToActionKey,
|
||||
path: string,
|
||||
translationKey:
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_trigger"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_condition"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_action"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName: string
|
||||
) {
|
||||
return html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type=${key}
|
||||
@click=${this._handleAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${path}></ha-svg-icon>
|
||||
${this._i18n.localize(translationKey, { target: areaName })}
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: Event) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = (ev.currentTarget as HTMLElement).dataset
|
||||
.type as AddToActionKey;
|
||||
|
||||
this.closeDialog();
|
||||
addToActionHandler(key, { area_id: this._params.areaId });
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of this._params.entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
showSceneEditor({ entities }, this._params.areaId);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-area-add-to": DialogAreaAddTo;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDevices,
|
||||
mdiDotsVertical,
|
||||
mdiImagePlus,
|
||||
mdiPalette,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiShape,
|
||||
mdiTools,
|
||||
} from "@mdi/js";
|
||||
import type {
|
||||
HassEntity,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket/dist/types";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -10,7 +25,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { goBack } from "../../../common/navigate";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { groupBy } from "../../../common/util/group-by";
|
||||
@@ -18,11 +33,13 @@ import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
@@ -38,6 +55,7 @@ import {
|
||||
computeEntityRegistryName,
|
||||
sortEntityRegistryByName,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { subscribeLabFeature } from "../../../data/labs";
|
||||
import type { SceneEntity } from "../../../data/scene";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
@@ -46,9 +64,15 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import "../../logbook/ha-logbook";
|
||||
import {
|
||||
loadAreaAddToDialog,
|
||||
showAreaAddToDialog,
|
||||
} from "./show-dialog-area-add-to";
|
||||
import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
@@ -59,8 +83,60 @@ declare interface NameAndEntity<EntityType extends HassEntity> {
|
||||
entity: EntityType;
|
||||
}
|
||||
|
||||
type AreaQuickLinkKey =
|
||||
| "devices"
|
||||
| "entities"
|
||||
| "helpers"
|
||||
| "automations"
|
||||
| "scenes"
|
||||
| "scripts";
|
||||
|
||||
const NAVIGATION_ACTIONS: {
|
||||
value: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
countKey: AreaQuickLinkKey;
|
||||
}[] = [
|
||||
{
|
||||
value: "navigate-devices",
|
||||
path: "/config/devices/dashboard",
|
||||
icon: mdiDevices,
|
||||
countKey: "devices",
|
||||
},
|
||||
{
|
||||
value: "navigate-entities",
|
||||
path: "/config/entities",
|
||||
icon: mdiShape,
|
||||
countKey: "entities",
|
||||
},
|
||||
{
|
||||
value: "navigate-helpers",
|
||||
path: "/config/helpers",
|
||||
icon: mdiTools,
|
||||
countKey: "helpers",
|
||||
},
|
||||
{
|
||||
value: "navigate-automations",
|
||||
path: "/config/automation/dashboard",
|
||||
icon: mdiRobot,
|
||||
countKey: "automations",
|
||||
},
|
||||
{
|
||||
value: "navigate-scenes",
|
||||
path: "/config/scene/dashboard",
|
||||
icon: mdiPalette,
|
||||
countKey: "scenes",
|
||||
},
|
||||
{
|
||||
value: "navigate-scripts",
|
||||
path: "/config/script/dashboard",
|
||||
icon: mdiScriptText,
|
||||
countKey: "scripts",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public areaId!: string;
|
||||
@@ -77,6 +153,8 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
@state() private _newTriggersConditions = false;
|
||||
|
||||
private _logbookTime = { recent: 86400 };
|
||||
|
||||
private _memberships = memoizeOne(
|
||||
@@ -128,9 +206,35 @@ class HaConfigAreaPage extends LitElement {
|
||||
.concat(memberships.indirectEntities.map((entry) => entry.entity_id))
|
||||
);
|
||||
|
||||
private _getQuickLinkCounts = memoizeOne(
|
||||
(
|
||||
memberships: {
|
||||
devices: DeviceRegistryEntry[];
|
||||
entities: EntityRegistryEntry[];
|
||||
indirectEntities: EntityRegistryEntry[];
|
||||
},
|
||||
related?: RelatedResult
|
||||
) => {
|
||||
const allEntityIds = this._allEntities(memberships);
|
||||
const entityIds = related?.entity ?? allEntityIds;
|
||||
|
||||
return {
|
||||
devices: related?.device?.length ?? memberships.devices.length,
|
||||
entities: entityIds.length,
|
||||
helpers: entityIds.filter((entityId) =>
|
||||
isHelperDomain(computeDomain(entityId))
|
||||
).length,
|
||||
automations: related?.automation?.length ?? 0,
|
||||
scenes: related?.scene?.length ?? 0,
|
||||
scripts: related?.script?.length ?? 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadAreaRegistryDetailDialog();
|
||||
loadAreaAddToDialog();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
@@ -140,6 +244,23 @@ class HaConfigAreaPage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
if (!isComponentLoaded(this.hass!.config, "automation")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersConditions = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass.areas || !this.hass.devices || !this.hass.entities) {
|
||||
return nothing;
|
||||
@@ -162,6 +283,10 @@ class HaConfigAreaPage extends LitElement {
|
||||
this._entityReg
|
||||
);
|
||||
const { devices, entities } = memberships;
|
||||
const quickLinkCounts = this._getQuickLinkCounts(
|
||||
memberships,
|
||||
this._related
|
||||
);
|
||||
|
||||
// Pre-compute the entity and device names, so we can sort by them
|
||||
if (devices) {
|
||||
@@ -245,6 +370,21 @@ class HaConfigAreaPage extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${NAVIGATION_ACTIONS.map(
|
||||
(action) => html`
|
||||
<ha-dropdown-item value=${action.value}>
|
||||
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.areas.quick_links.${action.countKey}`,
|
||||
{ count: quickLinkCounts[action.countKey] }
|
||||
)}
|
||||
<ha-icon-next slot="details"></ha-icon-next>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item value="edit" .data=${area}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}> </ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.edit_settings")}
|
||||
@@ -271,15 +411,41 @@ class HaConfigAreaPage extends LitElement {
|
||||
class="img-edit-btn"
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.add_picture")}
|
||||
</ha-button>`}
|
||||
: nothing}
|
||||
${area.picture && !this._newTriggersConditions
|
||||
? nothing
|
||||
: html`<div class="action-buttons">
|
||||
${area.picture
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiImagePlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.add_picture"
|
||||
)}
|
||||
</ha-button>`}
|
||||
${this._newTriggersConditions
|
||||
? html`<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`}
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
@@ -612,9 +778,39 @@ class HaConfigAreaPage extends LitElement {
|
||||
this._related = await findRelated(this.hass, "area", this.areaId);
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
private _showAddToDialog() {
|
||||
const area = this.hass.areas[this.areaId];
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
showAreaAddToDialog(this, {
|
||||
areaId: area.area_id,
|
||||
entityIds: this._areaEntityIds,
|
||||
});
|
||||
}
|
||||
|
||||
private get _areaEntityIds(): string[] {
|
||||
const memberships = this._memberships(
|
||||
this.areaId,
|
||||
Object.values(this.hass.devices),
|
||||
this._entityReg
|
||||
);
|
||||
|
||||
return this._allEntities(memberships);
|
||||
}
|
||||
|
||||
private _handleMenuAction(
|
||||
ev: HaDropdownSelectEvent<string, AreaRegistryEntry>
|
||||
) {
|
||||
const action = ev.detail?.item?.value;
|
||||
const entry = (ev.detail?.item as any)?.data as AreaRegistryEntry;
|
||||
const entry = ev.detail?.item?.data;
|
||||
|
||||
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
|
||||
if (navAction) {
|
||||
navigate(`${navAction.path}?historyBack=1&area=${this.areaId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "edit":
|
||||
this._openDialog(entry);
|
||||
@@ -625,15 +821,19 @@ class HaConfigAreaPage extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _showSettings(ev: MouseEvent) {
|
||||
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
private _showSettings(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLButtonElement & { entry: AreaRegistryEntry }
|
||||
>
|
||||
) {
|
||||
this._openDialog(ev.currentTarget.entry);
|
||||
}
|
||||
|
||||
private _openEntity(ev) {
|
||||
const entry: EntityRegistryEntry = (ev.currentTarget as any).entity;
|
||||
private _openEntity(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: EntityRegistryEntry }>
|
||||
) {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: entry.entity_id,
|
||||
entityId: ev.currentTarget.entity.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -675,6 +875,14 @@ class HaConfigAreaPage extends LitElement {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface AreaAddToDialogParams {
|
||||
areaId: string;
|
||||
entityIds: string[];
|
||||
}
|
||||
|
||||
export const loadAreaAddToDialog = () => import("./dialog-area-add-to");
|
||||
|
||||
export const showAreaAddToDialog = (
|
||||
element: HTMLElement,
|
||||
params: AreaAddToDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-area-add-to",
|
||||
dialogImport: loadAreaAddToDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -143,6 +143,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
|
||||
const addActionTargetFromQuery = getAddAutomationElementTargetFromQuery(
|
||||
this.hass.states,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
"action"
|
||||
);
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ import "./add-automation-element/ha-automation-add-items";
|
||||
import "./add-automation-element/ha-automation-add-search";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import {
|
||||
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
@@ -311,6 +312,7 @@ class DialogAddAutomationElement
|
||||
const queryTarget = getAddAutomationElementTargetFromQuery(
|
||||
this.hass.states,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
params.type
|
||||
);
|
||||
this._openedFromQuery = !!queryTarget;
|
||||
@@ -320,6 +322,7 @@ class DialogAddAutomationElement
|
||||
searchParams.delete(ADD_AUTOMATION_ELEMENT_QUERY_PARAM);
|
||||
searchParams.delete(ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM);
|
||||
searchParams.delete(ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM);
|
||||
searchParams.delete(ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM);
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state,
|
||||
"",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -587,7 +588,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
private _getTriggerInfos = memoizeOne(getTriggerInfos);
|
||||
|
||||
private _renderTriggerConditionDescription(condition: TriggerCondition) {
|
||||
const ids = ensureArray(condition.id ?? []).filter((id) => id !== "");
|
||||
const ids = ensureArray(condition.id ?? [])
|
||||
.map((id) => (typeof id === "string" ? id : String(id)))
|
||||
.filter((id) => id !== "");
|
||||
const prefix = capitalizeFirstLetter(
|
||||
this.hass
|
||||
.localize(
|
||||
@@ -605,44 +608,77 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const triggers = ensureArray(this._automationConfig?.triggers || []);
|
||||
|
||||
const triggerInfos = this._getTriggerInfos(
|
||||
triggers,
|
||||
ensureArray(this._automationConfig?.triggers || []),
|
||||
this.hass,
|
||||
this._entityReg
|
||||
);
|
||||
const infoById = new Map(triggerInfos.map((info) => [info.id, info]));
|
||||
return html`${prefix}
|
||||
${ids
|
||||
.filter((id) => infoById.get(id))
|
||||
.map((id) => {
|
||||
const info = infoById.get(id)!;
|
||||
${ids.map((id) => {
|
||||
const info = infoById.get(id);
|
||||
if (!info) {
|
||||
return html`<div class="trigger">
|
||||
<ha-trigger-id-chip id=${`trigger-${id}`} warning .triggerId=${id}>
|
||||
<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-trigger-id-chip>
|
||||
${ids.length < 4
|
||||
? html`<span
|
||||
>${this.hass.localize("state.default.unavailable")}</span
|
||||
>`
|
||||
: nothing}
|
||||
|
||||
const triggerIcon = html`<ha-trigger-icon
|
||||
.slot=${ids.length < 4 ? "start" : ""}
|
||||
.hass=${this.hass}
|
||||
.trigger=${info.triggerType}
|
||||
></ha-trigger-icon>`;
|
||||
|
||||
return html`
|
||||
<div class="trigger">
|
||||
${ids.length < 4 ? triggerIcon : nothing}
|
||||
<ha-trigger-id-chip id=${`trigger-${id}`} .triggerId=${id}>
|
||||
</ha-trigger-id-chip>
|
||||
${ids.length < 4
|
||||
? html`<span>${info.label}</span>`
|
||||
: html`<ha-tooltip .for=${`trigger-${id}`}></ha-tooltip>`}
|
||||
<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${ids.length >= 4
|
||||
? html`<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${ids.length >= 4
|
||||
? html`<div>${triggerIcon}${info.label}</div>`
|
||||
: nothing}
|
||||
</ha-tooltip>`
|
||||
? html`<div>
|
||||
${this.hass.localize("state.default.unavailable")}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}`;
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
|
||||
{ id: html`<b>${id}</b>` }
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>`;
|
||||
}
|
||||
const triggerIcon = html`<ha-trigger-icon
|
||||
.slot=${ids.length < 4 ? "start" : ""}
|
||||
.hass=${this.hass}
|
||||
.trigger=${info.triggerType}
|
||||
></ha-trigger-icon>`;
|
||||
|
||||
const isDuplicateId = info.count > 1;
|
||||
|
||||
return html`
|
||||
<div class="trigger">
|
||||
${ids.length < 4 ? triggerIcon : nothing}
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${id}`}
|
||||
.triggerId=${id}
|
||||
.warning=${isDuplicateId}
|
||||
>
|
||||
${isDuplicateId
|
||||
? html`<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${ids.length < 4
|
||||
? html`<span>${info.label}</span>`
|
||||
: html`<ha-tooltip .for=${`trigger-${id}`}></ha-tooltip>`}
|
||||
${isDuplicateId || ids.length >= 4
|
||||
? html`<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${ids.length >= 4
|
||||
? html`<div>${triggerIcon}${info.label}</div>`
|
||||
: nothing}
|
||||
${isDuplicateId
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
|
||||
)
|
||||
: nothing}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}`;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
|
||||
@@ -128,6 +128,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
|
||||
const addConditionTargetFromQuery = getAddAutomationElementTargetFromQuery(
|
||||
this.hass.states,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
"condition"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
@@ -62,8 +63,8 @@ export class HaTriggerCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const selectedIds: (string | number)[] = ensureArray(
|
||||
this.condition.id || []
|
||||
const selectedIds = ensureArray(this.condition.id || []).filter(
|
||||
(id): id is string => typeof id === "string" && id !== ""
|
||||
);
|
||||
|
||||
const triggerInfos = this._triggerInfos(
|
||||
@@ -88,11 +89,43 @@ export class HaTriggerCondition extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderOptions(
|
||||
selectedIds: (string | number)[],
|
||||
triggerInfos: TriggerInfo[]
|
||||
) {
|
||||
private _renderOptions(selectedIds: string[], triggerInfos: TriggerInfo[]) {
|
||||
const unknownTriggerIds = selectedIds.filter(
|
||||
(id) => !triggerInfos.some((info) => info.id === id)
|
||||
);
|
||||
|
||||
const alertIcon = html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAlert}
|
||||
></ha-svg-icon>`;
|
||||
|
||||
return html`
|
||||
${unknownTriggerIds.map(
|
||||
(id) => html`
|
||||
<ha-list-item-option
|
||||
.value=${id}
|
||||
.selected=${true}
|
||||
appearance="checkbox"
|
||||
>
|
||||
<div class="option" slot="headline">
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${id}`}
|
||||
warning
|
||||
.triggerId=${id}
|
||||
>
|
||||
${alertIcon}
|
||||
</ha-trigger-id-chip>
|
||||
${this.hass.localize("state.default.unavailable")}
|
||||
<ha-tooltip .for=${`trigger-${id}`}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
|
||||
{ id: html`<b>${id}</b>` }
|
||||
)}
|
||||
</ha-tooltip>
|
||||
</div>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
)}
|
||||
${triggerInfos.map(
|
||||
(info) => html`
|
||||
<ha-list-item-option
|
||||
@@ -103,10 +136,18 @@ export class HaTriggerCondition extends LitElement {
|
||||
<div class="option" slot="headline">
|
||||
<ha-trigger-id-chip
|
||||
id=${`trigger-${info.id}`}
|
||||
.warning=${info.count > 1}
|
||||
.triggerId=${info.id}
|
||||
>
|
||||
${info.count > 1 ? alertIcon : nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${info.label}
|
||||
${info.label}${info.count > 1
|
||||
? html`<ha-tooltip .for=${`trigger-${info.id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.trigger.duplicated_info"
|
||||
)}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-list-item-option>
|
||||
`
|
||||
|
||||
@@ -768,10 +768,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
const hasUrlFilter =
|
||||
this._searchParms.has("blueprint") || this._searchParms.has("label");
|
||||
this._searchParms.has("area") ||
|
||||
this._searchParms.has("blueprint") ||
|
||||
this._searchParms.has("device") ||
|
||||
this._searchParms.has("label");
|
||||
if (!hasUrlFilter) {
|
||||
this._filters = this._storageFilters;
|
||||
}
|
||||
if (this._searchParms.has("area")) {
|
||||
this._filterArea();
|
||||
}
|
||||
if (this._searchParms.has("device")) {
|
||||
this._filterDevice();
|
||||
}
|
||||
if (this._searchParms.has("blueprint")) {
|
||||
this._filterBlueprint();
|
||||
}
|
||||
@@ -871,6 +880,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._filteredEntityIds = filteredEntityIds;
|
||||
}
|
||||
|
||||
private _filterArea() {
|
||||
const area = this._searchParms.get("area");
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: [area] },
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterDevice() {
|
||||
const device = this._searchParms.get("device");
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-devices": {
|
||||
value: [device],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
|
||||
@@ -8,6 +8,7 @@ export const PASTE_VALUE = "__paste__";
|
||||
export const ADD_AUTOMATION_ELEMENT_QUERY_PARAM = "add_automation_element";
|
||||
export const ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM = "target_entity_id";
|
||||
export const ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM = "target_device_id";
|
||||
export const ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM = "target_area_id";
|
||||
|
||||
/** Parameters for the add automation element dialog. */
|
||||
export interface AddAutomationElementDialogParams {
|
||||
@@ -21,6 +22,7 @@ export interface AddAutomationElementDialogParams {
|
||||
export const getAddAutomationElementTargetFromQuery = (
|
||||
states: HomeAssistant["states"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
type: AddAutomationElementDialogParams["type"]
|
||||
): SingleHassServiceTarget | undefined => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -39,6 +41,11 @@ export const getAddAutomationElementTargetFromQuery = (
|
||||
return { device_id: deviceId };
|
||||
}
|
||||
|
||||
const areaId = params.get(ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM);
|
||||
if (areaId && areas[areaId]) {
|
||||
return { area_id: areaId };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,15 +14,13 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
@@ -37,7 +35,6 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import "../ha-automation-comment";
|
||||
import "../ha-trigger-id-chip";
|
||||
import { overflowStyles, sidebarEditorStyles } from "../styles";
|
||||
import "../trigger/ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
|
||||
@@ -60,6 +57,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@property({ type: Number, attribute: "sidebar-key" })
|
||||
public sidebarKey?: number;
|
||||
|
||||
@state() private _requestShowId = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query(".sidebar-editor")
|
||||
@@ -67,6 +66,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("config")) {
|
||||
this._requestShowId = false;
|
||||
this._warnings = undefined;
|
||||
if (this.config) {
|
||||
this.yamlMode = this.config.yamlMode;
|
||||
@@ -111,21 +111,11 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<span slot="title">${title}</span>
|
||||
<div slot="subtitle" class="subtitle">
|
||||
${subtitle}
|
||||
${"id" in this.config.config
|
||||
? html`<ha-trigger-id-chip
|
||||
id="trigger-id-chip"
|
||||
.triggerId=${(
|
||||
this.config.config as Exclude<Trigger, TriggerList>
|
||||
).id}
|
||||
>
|
||||
</ha-trigger-id-chip>`
|
||||
: nothing}
|
||||
${rowDisabled
|
||||
? `(${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
|
||||
: nothing}
|
||||
</div>
|
||||
<span slot="subtitle"
|
||||
>${subtitle}${rowDisabled
|
||||
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
|
||||
: ""}</span
|
||||
>
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="rename"
|
||||
@@ -157,16 +147,18 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
</div>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${type !== "list"
|
||||
? html` <ha-dropdown-item
|
||||
${!this.yamlMode &&
|
||||
!("id" in this.config.config) &&
|
||||
!this._requestShowId
|
||||
? html`<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="edit_id"
|
||||
value="show_id"
|
||||
.disabled=${this.disabled || type === "list"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.${"id" in this.config.config ? "edit" : "add"}_id`
|
||||
"ui.panel.config.automation.editor.triggers.edit_id"
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
@@ -343,6 +335,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
@yaml-changed=${this._yamlChangedSidebar}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
.showId=${this._requestShowId}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.disabled=${this.disabled}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
@@ -393,6 +386,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
fireEvent(this, "toggle-yaml-mode");
|
||||
};
|
||||
|
||||
private _showTriggerId = () => {
|
||||
this._requestShowId = true;
|
||||
};
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
@@ -407,8 +404,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
case "edit_comment":
|
||||
this.config.editComment();
|
||||
break;
|
||||
case "edit_id":
|
||||
this.config.editId();
|
||||
case "show_id":
|
||||
this._showTriggerId();
|
||||
break;
|
||||
case "duplicate":
|
||||
this.config.duplicate();
|
||||
@@ -434,16 +431,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
sidebarEditorStyles,
|
||||
overflowStyles,
|
||||
css`
|
||||
.subtitle {
|
||||
display: flex;
|
||||
gap: var(--ha-space-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
static styles = [sidebarEditorStyles, overflowStyles];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
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/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { EditTriggerIdDialogParams } from "./show-edit-trigger-id";
|
||||
|
||||
@customElement("ha-automation-edit-trigger-id-dialog")
|
||||
class HaAutomationEditTriggerIdDialog extends DialogMixin<EditTriggerIdDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state() private _newId = "";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._setInitialId();
|
||||
}
|
||||
|
||||
private _setInitialId() {
|
||||
if (this.params?.id) {
|
||||
this._newId = this.params.id;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const title = this._i18n.localize(
|
||||
`ui.panel.config.automation.editor.triggers.${
|
||||
this.params.id ? "edit_id" : "add_id"
|
||||
}`
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog open header-title=${title}>
|
||||
<ha-input
|
||||
autofocus
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this._newId}
|
||||
@input=${this._idChanged}
|
||||
@keydown=${this._handleKeyDown}
|
||||
></ha-input>
|
||||
<ha-alert alert-type="info">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._i18n.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._save}>
|
||||
${this._i18n.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _idChanged(ev: InputEvent) {
|
||||
const target = ev.target as HaInput;
|
||||
this._newId = target.value ?? "";
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
private _save(): void {
|
||||
const trimmed = this._newId.trim();
|
||||
this.params!.onUpdate(trimmed || undefined);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-edit-trigger-id-dialog": HaAutomationEditTriggerIdDialog;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { dynamicElement } from "../../../../common/dom/dynamic-element-directive
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { Trigger } from "../../../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../../../data/automation";
|
||||
import type { TriggerDescription } from "../../../../data/trigger";
|
||||
@@ -30,6 +31,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
|
||||
|
||||
@property({ attribute: false }) public description?: TriggerDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
@@ -39,6 +42,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
|
||||
const yamlMode = this.yamlMode || !this.uiSupported;
|
||||
|
||||
const showId = "id" in this.trigger || this.showId;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -72,6 +77,18 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${showId && !isTriggerList(this.trigger)
|
||||
? html`
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.id"
|
||||
)}
|
||||
.value=${this.trigger.id || ""}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._idChanged}
|
||||
></ha-input>
|
||||
`
|
||||
: nothing}
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${this.description
|
||||
? html`<ha-automation-trigger-platform
|
||||
@@ -91,6 +108,24 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _idChanged(ev: CustomEvent) {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const newId = (ev.target as any).value;
|
||||
|
||||
if (newId === (this.trigger.id ?? "")) {
|
||||
return;
|
||||
}
|
||||
const value = { ...this.trigger };
|
||||
if (!newId) {
|
||||
delete value.id;
|
||||
} else {
|
||||
value.id = newId;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
@@ -125,6 +160,9 @@ export default class HaAutomationTriggerEditor extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
ha-input {
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
mdiContentPaste,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiIdentifier,
|
||||
mdiPlayCircleOutline,
|
||||
mdiPlaylistEdit,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
@@ -29,6 +29,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -53,12 +54,17 @@ import "../../../../components/ha-tooltip";
|
||||
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
AutomationConfig,
|
||||
PlatformTrigger,
|
||||
Trigger,
|
||||
TriggerList,
|
||||
TriggerSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import {
|
||||
automationConfigContext,
|
||||
isTrigger,
|
||||
subscribeTrigger,
|
||||
} from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
@@ -80,7 +86,6 @@ import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-trigger-editor";
|
||||
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
|
||||
import { showEditTriggerIdDialog } from "./show-edit-trigger-id";
|
||||
import "./types/ha-automation-trigger-calendar";
|
||||
import "./types/ha-automation-trigger-conversation";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
@@ -182,6 +187,30 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: automationConfigContext, subscribe: true })
|
||||
@transform<AutomationConfig, boolean>({
|
||||
transformer: function (this: HaAutomationTriggerRow, value) {
|
||||
if (
|
||||
!this.trigger ||
|
||||
isTriggerList(this.trigger) ||
|
||||
!(this.trigger as Exclude<Trigger, TriggerList>).id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const triggerId = (this.trigger as Exclude<Trigger, TriggerList>).id;
|
||||
// count how often this trigger id is used in the automation, if more than once, show warning
|
||||
return (
|
||||
ensureArray(value?.triggers || []).filter(
|
||||
(trigger) =>
|
||||
(trigger as Exclude<Trigger, TriggerList>).id === triggerId
|
||||
).length > 1
|
||||
);
|
||||
},
|
||||
watch: ["trigger"],
|
||||
})
|
||||
private _duplicateTriggerId = false;
|
||||
|
||||
get selected() {
|
||||
return this._selected;
|
||||
}
|
||||
@@ -250,11 +279,25 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
${type !== "list" && (this.trigger as Exclude<Trigger, TriggerList>).id
|
||||
? html`<ha-trigger-id-chip
|
||||
id="trigger-id-chip"
|
||||
slot="leading-icon"
|
||||
.triggerId=${(this.trigger as Exclude<Trigger, TriggerList>).id}
|
||||
>
|
||||
</ha-trigger-id-chip>`
|
||||
id="trigger-id-chip"
|
||||
.warning=${this._duplicateTriggerId}
|
||||
slot="leading-icon"
|
||||
.triggerId=${(this.trigger as Exclude<Trigger, TriggerList>).id}
|
||||
>
|
||||
${this._duplicateTriggerId
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAlert}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-trigger-id-chip>
|
||||
${this._duplicateTriggerId
|
||||
? html`<ha-tooltip for="trigger-id-chip">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing} `
|
||||
: nothing}
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${target !== undefined || (descriptionHasTarget && !this._isNew)
|
||||
@@ -333,17 +376,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${type !== "list"
|
||||
? html`<ha-dropdown-item value="edit_id" .disabled=${this.disabled}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiIdentifier}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.${"id" in this.trigger ? "edit" : "add"}_id`
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
|
||||
@@ -717,7 +749,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
this.focus();
|
||||
}
|
||||
},
|
||||
editId: this._editTriggerId,
|
||||
rename: () => {
|
||||
this._renameTrigger();
|
||||
},
|
||||
@@ -829,34 +860,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _editTriggerId = () => {
|
||||
if (isTriggerList(this.trigger)) {
|
||||
return;
|
||||
}
|
||||
const trigger = this.trigger as Exclude<Trigger, TriggerList>;
|
||||
showEditTriggerIdDialog(this, {
|
||||
id: trigger.id,
|
||||
onUpdate: (newId) => {
|
||||
if (newId === (trigger.id ?? undefined)) {
|
||||
return;
|
||||
}
|
||||
const value: Trigger = { ...trigger };
|
||||
if (newId) {
|
||||
value.id = newId;
|
||||
} else {
|
||||
delete value.id;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(value); // refresh sidebar
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _renameTrigger = async (): Promise<void> => {
|
||||
if (isTriggerList(this.trigger)) return;
|
||||
const alias = await showPromptDialog(this, {
|
||||
@@ -1041,9 +1044,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
case "edit_comment":
|
||||
this._editCommentTrigger();
|
||||
break;
|
||||
case "edit_id":
|
||||
this._editTriggerId();
|
||||
break;
|
||||
case "duplicate":
|
||||
this._duplicateTrigger();
|
||||
break;
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
} from "../../../../data/automation";
|
||||
import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
||||
import {
|
||||
getNextNumericTriggerId,
|
||||
getUniqueTriggerId,
|
||||
isTriggerList,
|
||||
subscribeTriggers,
|
||||
} from "../../../../data/trigger";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
|
||||
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
|
||||
@@ -71,6 +76,11 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
protected override pasteItem(ev: CustomEvent) {
|
||||
if (this.root && ev.detail.item) {
|
||||
const pasted = deepClone(ev.detail.item) as Trigger;
|
||||
if (!isTriggerList(pasted)) {
|
||||
pasted.id = pasted.id
|
||||
? getUniqueTriggerId(pasted.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
ev.detail.item = pasted;
|
||||
}
|
||||
super.pasteItem(ev);
|
||||
@@ -81,6 +91,11 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
const incoming = ensureArray(ev.detail.value) as Trigger[];
|
||||
if (this.root && incoming.length === 1) {
|
||||
const trigger = deepClone(incoming[0]);
|
||||
if (!isTriggerList(trigger)) {
|
||||
trigger.id = trigger.id
|
||||
? getUniqueTriggerId(trigger.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
ev.detail.value = trigger;
|
||||
}
|
||||
super.insertAfter(ev);
|
||||
@@ -90,6 +105,11 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
if (this.root) {
|
||||
const index = (ev.target as any).index;
|
||||
const duplicated = deepClone(this.triggers[index]);
|
||||
if (!isTriggerList(duplicated)) {
|
||||
duplicated.id = duplicated.id
|
||||
? getUniqueTriggerId(duplicated.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
// @ts-expect-error Requires library bump to ES2023
|
||||
value: this.triggers.toSpliced(index + 1, 0, duplicated),
|
||||
@@ -247,6 +267,11 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
const pasted = deepClone(this._clipboard!.trigger!);
|
||||
if (this.root && !isTriggerList(pasted)) {
|
||||
pasted.id = pasted.id
|
||||
? getUniqueTriggerId(pasted.id, this.triggers)
|
||||
: getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
triggers = this.triggers.concat(pasted);
|
||||
} else {
|
||||
let newTrigger: Trigger;
|
||||
@@ -267,6 +292,9 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
|
||||
};
|
||||
}
|
||||
if (this.root && !isTriggerList(newTrigger)) {
|
||||
newTrigger.id = getNextNumericTriggerId(this.triggers);
|
||||
}
|
||||
triggers = this.triggers.concat(newTrigger);
|
||||
}
|
||||
this.focusLastItemOnChange = true;
|
||||
@@ -283,6 +311,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
const addTriggerTargetFromQuery = getAddAutomationElementTargetFromQuery(
|
||||
this.hass.states,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
"trigger"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { LitElement } from "lit";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export const loadEditTriggerIdDialog = () =>
|
||||
import("./ha-automation-edit-trigger-id-dialog");
|
||||
|
||||
export interface EditTriggerIdDialogParams {
|
||||
id?: string;
|
||||
onUpdate: (newId: string | undefined) => void;
|
||||
}
|
||||
|
||||
export const showEditTriggerIdDialog = (
|
||||
element: LitElement,
|
||||
dialogParams: EditTriggerIdDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
parentElement: element,
|
||||
dialogTag: "ha-automation-edit-trigger-id-dialog",
|
||||
dialogImport: loadEditTriggerIdDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday";
|
||||
@@ -225,6 +225,13 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
|
||||
);
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
|
||||
@@ -20,8 +20,8 @@ class HaBackupConfigAddon extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.schedule.update_preference.label`
|
||||
@@ -52,8 +52,8 @@ class HaBackupConfigAddon extends LitElement {
|
||||
},
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
|
||||
</span>
|
||||
@@ -77,8 +77,8 @@ class HaBackupConfigAddon extends LitElement {
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -106,13 +106,12 @@ class HaBackupConfigAddon extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
|
||||
@@ -7,10 +7,10 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupAgentsConfig,
|
||||
@@ -181,7 +181,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
return html`
|
||||
${allAgents.length > 0
|
||||
? html`
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
${availableAgents.map((agent) => {
|
||||
const agentId = agent.agent_id;
|
||||
const name = computeBackupAgentName(
|
||||
@@ -196,7 +196,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
!this.cloudStatus.active_subscription;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${this._renderAgentIcon(agentId)}
|
||||
<div slot="headline" class="name">${name}</div>
|
||||
${description
|
||||
@@ -220,7 +220,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
!this._value.includes(agentId)}
|
||||
@change=${this._agentToggled}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
})}
|
||||
${unavailableAgents.length > 0 && this.showSettings
|
||||
@@ -239,7 +239,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${this._renderAgentIcon(agentId)}
|
||||
<div slot="headline" class="name">${name}</div>
|
||||
<ha-icon-button
|
||||
@@ -248,12 +248,12 @@ class HaBackupConfigAgents extends LitElement {
|
||||
path=${mdiDelete}
|
||||
@click=${this._deleteAgent}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
})}
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
`
|
||||
: html`
|
||||
<p>
|
||||
@@ -293,30 +293,25 @@ class HaBackupConfigAgents extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
ha-md-list-item .name {
|
||||
ha-list-item-base .name {
|
||||
word-break: break-word;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
ha-list-item-base img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
ha-list-item-base ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-list-item-base::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
.unencrypted-warning {
|
||||
display: flex;
|
||||
@@ -338,7 +333,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
ha-list-item-base [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -15,13 +15,13 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../../components/ha-switch";
|
||||
import "../../../../../components/ha-tooltip";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon";
|
||||
import type { HostDisksUsage } from "../../../../../data/hassio/host";
|
||||
import { fetchHostDisksUsage } from "../../../../../data/hassio/host";
|
||||
@@ -238,8 +238,8 @@ class HaBackupConfigData extends LitElement {
|
||||
|
||||
return html`
|
||||
${this._renderSizeEstimate()}
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.backup.data.ha_settings")}
|
||||
@@ -260,10 +260,10 @@ class HaBackupConfigData extends LitElement {
|
||||
.checked=${data.homeassistant}
|
||||
.disabled=${this.forceHomeAssistant || data.database}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
|
||||
${this._showDbOption
|
||||
? html`<ha-md-list-item>
|
||||
? html`<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.backup.data.history")}
|
||||
@@ -279,11 +279,11 @@ class HaBackupConfigData extends LitElement {
|
||||
@change=${this._switchChanged}
|
||||
.checked=${data.database}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>`
|
||||
</ha-list-item-base>`
|
||||
: nothing}
|
||||
${isHassio
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
@@ -302,9 +302,9 @@ class HaBackupConfigData extends LitElement {
|
||||
@change=${this._switchChanged}
|
||||
.checked=${data.media}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiFolder}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -322,11 +322,11 @@ class HaBackupConfigData extends LitElement {
|
||||
@change=${this._switchChanged}
|
||||
.checked=${data.share}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
|
||||
${this._hasLocalAddons(this._addons)
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiFolder}
|
||||
@@ -347,12 +347,12 @@ class HaBackupConfigData extends LitElement {
|
||||
@change=${this._switchChanged}
|
||||
.checked=${data.local_addons}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing}
|
||||
${this._addons.length
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPuzzle}
|
||||
@@ -392,12 +392,12 @@ class HaBackupConfigData extends LitElement {
|
||||
},
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
${isHassio && this._showAddons && this._addons.length
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
@@ -551,13 +551,15 @@ class HaBackupConfigData extends LitElement {
|
||||
ha-spinner {
|
||||
--ha-spinner-size: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-list-item-base::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { mdiDownload } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key";
|
||||
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
|
||||
@@ -24,8 +24,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
protected render() {
|
||||
if (this._value) {
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
@@ -47,8 +47,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.show_encryption_key"
|
||||
@@ -69,8 +69,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
"ui.panel.config.backup.encryption_key.show_encryption_key_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.change_encryption_key"
|
||||
@@ -92,14 +92,14 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
"ui.panel.config.backup.encryption_key.change_encryption_key_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.set_encryption_key"
|
||||
@@ -115,8 +115,8 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
"ui.panel.config.backup.encryption_key.set_encryption_key_action"
|
||||
)}</ha-button
|
||||
>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -149,15 +149,13 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
|
||||
ha-button[size="small"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { clamp } from "../../../../../common/number/clamp";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-row-item";
|
||||
import type { BackupConfig, Retention } from "../../../../../data/backup";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
|
||||
@@ -104,7 +105,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.headline ??
|
||||
this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
|
||||
@@ -125,7 +126,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
),
|
||||
}))}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
|
||||
${this._preset === RetentionPreset.CUSTOM
|
||||
? html`<ha-expansion-panel
|
||||
@@ -135,7 +136,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.custom_retention_label"
|
||||
@@ -171,7 +172,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
),
|
||||
},
|
||||
]}
|
||||
></ha-select> </ha-md-list-item
|
||||
></ha-select></ha-row-item
|
||||
></ha-expansion-panel> `
|
||||
: nothing}
|
||||
`;
|
||||
@@ -244,10 +245,17 @@ class HaBackupConfigRetention extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-row-item,
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(end) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
@@ -279,9 +287,6 @@ class HaBackupConfigRetention extends LitElement {
|
||||
--expansion-panel-content-padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-md-list-item.days {
|
||||
--md-item-align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-time-input";
|
||||
import "../../../../../components/ha-tip";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-row-item";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type {
|
||||
BackupConfig,
|
||||
BackupDay,
|
||||
@@ -116,8 +117,8 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
const data = this._getData(this.value);
|
||||
|
||||
return html`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.schedule"
|
||||
@@ -140,7 +141,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
),
|
||||
}))}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
${data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS
|
||||
? html`<ha-expansion-panel
|
||||
expanded
|
||||
@@ -149,7 +150,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-md-list-item class="days">
|
||||
<ha-row-item class="days">
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.backup_every"
|
||||
@@ -172,14 +173,14 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
</ha-expansion-panel>`
|
||||
: nothing}
|
||||
${data.recurrence === BackupScheduleRecurrence.DAILY ||
|
||||
(data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS &&
|
||||
data.days.length > 0)
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.time"
|
||||
@@ -214,7 +215,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
),
|
||||
}))}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
${data.time_option === BackupScheduleTime.CUSTOM
|
||||
? html`<ha-expansion-panel
|
||||
expanded
|
||||
@@ -223,7 +224,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.custom_time_label"
|
||||
@@ -248,14 +249,14 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
.locale=${this.hass.locale}
|
||||
>
|
||||
</ha-time-input>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
</ha-expansion-panel>`
|
||||
: nothing}
|
||||
`
|
||||
: nothing}
|
||||
${this.supervisor
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.schedule.update_preference.label`
|
||||
@@ -286,7 +287,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
},
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -308,7 +309,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
>`,
|
||||
})}</ha-tip
|
||||
>
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -398,13 +399,14 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text),
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
@@ -430,8 +432,8 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
text-align: unset;
|
||||
margin: 16px 0;
|
||||
}
|
||||
ha-md-list-item.days {
|
||||
--md-item-align-items: flex-start;
|
||||
ha-row-item-base.days::part(end) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
|
||||
@@ -2,8 +2,6 @@ import memoizeOne from "memoize-one";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-button";
|
||||
import "./ha-backup-data-picker";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -120,22 +118,6 @@ class HaBackupDetailsRestore extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import {
|
||||
computeBackupSize,
|
||||
computeBackupType,
|
||||
@@ -59,8 +59,8 @@ class HaBackupDetailsSummary extends LitElement {
|
||||
</div>
|
||||
<div class="card-content">
|
||||
${errors.length ? this._renderErrorSummary(errors) : nothing}
|
||||
<ha-md-list class="summary">
|
||||
<ha-md-list-item>
|
||||
<ha-list-base class="summary">
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.backup.backup_type")}
|
||||
</span>
|
||||
@@ -69,8 +69,8 @@ class HaBackupDetailsSummary extends LitElement {
|
||||
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
|
||||
)}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.size"
|
||||
@@ -79,16 +79,16 @@ class HaBackupDetailsSummary extends LitElement {
|
||||
<span slot="supporting-text">
|
||||
${bytesToString(computeBackupSize(this.backup))}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.details.summary.created"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">${formattedDate}</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -148,23 +148,17 @@ class HaBackupDetailsSummary extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
padding-bottom: var(--ha-space-3);
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
ha-list-base.summary ha-list-item-base::part(headline) {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
ha-md-list.summary ha-md-list-item {
|
||||
--md-list-item-supporting-text-size: 1rem;
|
||||
--md-list-item-label-text-size: 0.875rem;
|
||||
|
||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
ha-list-item-base [slot="supporting-text"] {
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--ha-color-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
+6
-14
@@ -4,9 +4,9 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import {
|
||||
getSupervisorUpdateConfig,
|
||||
type SupervisorUpdateConfig,
|
||||
@@ -73,11 +73,8 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/app-update-backups"
|
||||
>
|
||||
<ha-list-nav>
|
||||
<ha-list-item-button href="/config/backup/app-update-backups">
|
||||
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
|
||||
<div slot="headline">${this._appUpdateBackupDescription()}</div>
|
||||
<div slot="supporting-text">
|
||||
@@ -86,8 +83,8 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
@@ -106,11 +103,6 @@ class HaBackupOverviewAppUpdateBackup extends LitElement {
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { isComponentLoaded } from "../../../../../common/config/is_component_loa
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import type { BackupContent, BackupType } from "../../../../../data/backup";
|
||||
import {
|
||||
computeBackupSize,
|
||||
@@ -69,13 +69,10 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
${this.hass.localize("ui.panel.config.backup.overview.backups.title")}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-list-nav>
|
||||
${stats.map(
|
||||
([type, { count, size }]) => html`
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/backups?type=${type}"
|
||||
>
|
||||
<ha-list-item-button href="/config/backup/backups?type=${type}">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${TYPE_ICONS[type]}
|
||||
@@ -93,10 +90,10 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="filled" href="/config/backup/backups?type=all">
|
||||
@@ -134,6 +131,9 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-list-item-button::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type { BackupAgent } from "../../../../../data/backup";
|
||||
import {
|
||||
computeBackupAgentName,
|
||||
@@ -332,7 +332,7 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
? this._handleAgentCollapseEnd
|
||||
: undefined}
|
||||
>
|
||||
<ha-md-list class="agent-list">
|
||||
<ha-list-base class="agent-list">
|
||||
${this.agents.map((agent) => {
|
||||
const name = computeBackupAgentName(
|
||||
this.hass.localize,
|
||||
@@ -344,7 +344,7 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
if (agentPercent !== undefined) {
|
||||
if (agentPercent >= 100) {
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
@@ -357,11 +357,11 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
class="agent-complete"
|
||||
.path=${mdiCheck}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
@@ -373,12 +373,12 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
${agentPercent}%
|
||||
</span>
|
||||
<ha-spinner slot="end" size="tiny"></ha-spinner>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${this._renderAgentIcon(agent.agent_id)}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">
|
||||
@@ -387,10 +387,10 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-spinner slot="end" size="tiny"></ha-spinner>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -506,14 +506,13 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
margin-top: var(--ha-space-4);
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
ha-list-item-base img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
ha-list-item-base ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -521,7 +520,7 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import { navigate } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-button";
|
||||
import "../../../../../components/list/ha-list-nav";
|
||||
import type { BackupAgent, BackupConfig } from "../../../../../data/backup";
|
||||
import {
|
||||
BackupScheduleRecurrence,
|
||||
@@ -213,11 +213,8 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/settings#schedule"
|
||||
>
|
||||
<ha-list-nav>
|
||||
<ha-list-item-button href="/config/backup/settings#schedule">
|
||||
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._scheduleDescription(this.config)}
|
||||
@@ -228,8 +225,8 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item type="link" href="/config/backup/settings#data">
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button href="/config/backup/settings#data">
|
||||
<ha-svg-icon slot="start" .path=${mdiDatabase}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._showDbOption &&
|
||||
@@ -247,13 +244,10 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
${isHassio
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/settings#data"
|
||||
>
|
||||
<ha-list-item-button href="/config/backup/settings#data">
|
||||
<ha-svg-icon slot="start" .path=${mdiPuzzle}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._addonsDescription(this.config)}
|
||||
@@ -264,13 +258,10 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/settings#locations"
|
||||
>
|
||||
<ha-list-item-button href="/config/backup/settings#locations">
|
||||
<ha-svg-icon slot="start" .path=${mdiUpload}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this._locationsDescription(this.config)}
|
||||
@@ -281,8 +272,8 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._configure} appearance="filled">
|
||||
@@ -321,6 +312,9 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-list-item-button::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import type { LocalizeKeys } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/list/ha-list-base";
|
||||
import type { BackupConfig, BackupContent } from "../../../../../data/backup";
|
||||
import {
|
||||
BackupScheduleRecurrence,
|
||||
@@ -65,15 +65,15 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<ha-backup-summary-card .heading=${heading} .status=${status}>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
|
||||
<span slot="headline" class=${headline === null ? "skeleton" : ""}
|
||||
>${headline}</span
|
||||
>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
${description || description === null
|
||||
? html`<ha-md-list-item>
|
||||
? html`<ha-list-item-base>
|
||||
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
|
||||
<span
|
||||
slot="headline"
|
||||
@@ -90,9 +90,9 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-md-list-item>`
|
||||
</ha-list-item-base>`
|
||||
: nothing}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
</ha-backup-summary-card>
|
||||
`;
|
||||
}
|
||||
@@ -347,13 +347,12 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
justify-content: flex-end;
|
||||
border-top: none;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
--ha-row-item-min-height: 40x;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-top-space: 8px;
|
||||
--md-list-item-bottom-space: 8px;
|
||||
--md-list-item-one-line-container-height: 40x;
|
||||
ha-list-item-base::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
span.skeleton {
|
||||
position: relative;
|
||||
|
||||
@@ -11,9 +11,10 @@ import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import type {
|
||||
BackupConfig,
|
||||
BackupMutableConfig,
|
||||
@@ -366,36 +367,34 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
@click=${this._downloadKey}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
@click=${this._downloadKey}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`;
|
||||
case "setup":
|
||||
return html`
|
||||
<ha-md-list class="full">
|
||||
<ha-md-list-item type="button" @click=${this._useRecommended}>
|
||||
<ha-list-base class="full">
|
||||
<ha-list-item-button @click=${this._useRecommended}>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.onboarding.setup.recommended_heading"
|
||||
@@ -407,8 +406,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item type="button" @click=${this._nextStep}>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button @click=${this._nextStep}>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.onboarding.setup.custom_heading"
|
||||
@@ -420,8 +419,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
`;
|
||||
case "schedule":
|
||||
return html`
|
||||
@@ -547,16 +546,12 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
--ha-dialog-max-height: min(605px, 100% - 48px);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list.full {
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
ha-list-base.full {
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
margin: 0 calc(-1 * var(--ha-space-6));
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -5,12 +5,11 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import {
|
||||
downloadEmergencyKit,
|
||||
generateEncryptionKey,
|
||||
@@ -160,26 +159,28 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@click=${this._copyOldKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button slot="end" @click=${this._downloadOld}>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="filled"
|
||||
@click=${this._downloadOld}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_old_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`;
|
||||
case "new":
|
||||
return html`
|
||||
@@ -195,26 +196,28 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button slot="end" @click=${this._downloadNew}>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
slot="end"
|
||||
appearance="filled"
|
||||
@click=${this._downloadNew}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`;
|
||||
case "done":
|
||||
return html`
|
||||
@@ -281,10 +284,8 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
.encryption-key {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -12,11 +12,10 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../components/input/ha-input";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
@@ -296,41 +295,39 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
@change=${this._nameChanged}
|
||||
>
|
||||
</ha-input>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-select
|
||||
slot="end"
|
||||
@selected=${this._selectChanged}
|
||||
.value=${this._formData.agents_mode}
|
||||
.options=${[
|
||||
{
|
||||
value: "all",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_options.all",
|
||||
{ count: this._allAgentIds.length }
|
||||
),
|
||||
disabled: !!disabledAgentIds.length,
|
||||
},
|
||||
{
|
||||
value: "custom",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_options.custom"
|
||||
),
|
||||
},
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-select
|
||||
slot="end"
|
||||
@selected=${this._selectChanged}
|
||||
.value=${this._formData.agents_mode}
|
||||
.options=${[
|
||||
{
|
||||
value: "all",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_options.all",
|
||||
{ count: this._allAgentIds.length }
|
||||
),
|
||||
disabled: !!disabledAgentIds.length,
|
||||
},
|
||||
{
|
||||
value: "custom",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.dialogs.generate.sync.locations_options.custom"
|
||||
),
|
||||
},
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-row-item>
|
||||
${disabledAgentIds.length
|
||||
? html`
|
||||
<ha-alert
|
||||
@@ -443,24 +440,19 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-md-list-item ha-select {
|
||||
ha-row-item ha-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
@media all and (max-width: 450px) {
|
||||
ha-md-list-item ha-select {
|
||||
ha-row-item ha-select {
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
ha-md-list-item ha-select > span {
|
||||
ha-row-item ha-select > span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -3,11 +3,11 @@ 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 "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/item/ha-list-item-button";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -52,7 +52,7 @@ class DialogNewBackup extends LitElement implements HassDialog {
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-md-list
|
||||
<ha-list-base
|
||||
innerRole="listbox"
|
||||
itemRoles="option"
|
||||
.innerAriaLabel=${this.hass.localize(
|
||||
@@ -60,9 +60,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
|
||||
)}
|
||||
rootTabbable
|
||||
>
|
||||
<ha-md-list-item
|
||||
<ha-list-item-button
|
||||
@click=${this._automatic}
|
||||
type="button"
|
||||
.disabled=${!this._params.config.create_backup.password}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
|
||||
@@ -77,8 +76,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item @click=${this._manual} type="button">
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button @click=${this._manual}>
|
||||
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -91,8 +90,8 @@ class DialogNewBackup extends LitElement implements HassDialog {
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-list-item-button>
|
||||
</ha-list-base>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
@@ -115,10 +114,6 @@ class DialogNewBackup extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import {
|
||||
downloadEmergencyKit,
|
||||
generateEncryptionKey,
|
||||
@@ -135,31 +134,29 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
@click=${this._download}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
slot="end"
|
||||
@click=${this._download}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`;
|
||||
case "done":
|
||||
return html`
|
||||
@@ -209,10 +206,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
.encryption-key {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -5,12 +5,11 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import { downloadEmergencyKit } from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
@@ -74,26 +73,24 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button slot="end" @click=${this._download}>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button slot="end" appearance="filled" @click=${this._download}>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit_action"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
@@ -128,10 +125,8 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
.encryption-key {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -12,16 +12,17 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupConfig,
|
||||
@@ -44,7 +45,6 @@ import "./components/ha-backup-details-restore";
|
||||
import "./components/ha-backup-details-summary";
|
||||
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
|
||||
import { downloadBackup } from "./helper/download_backup";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
interface Agent extends BackupContentAgent {
|
||||
id: string;
|
||||
@@ -171,7 +171,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
${this._agents.map((agent) => {
|
||||
const agentId = agent.id;
|
||||
|
||||
@@ -186,7 +186,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
const unencrypted = !agent.protected;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
${
|
||||
isLocalAgent(agentId)
|
||||
? html`
|
||||
@@ -282,10 +282,10 @@ class HaConfigBackupDetails extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
@@ -374,26 +374,20 @@ class HaConfigBackupDetails extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
ha-list-item-base img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
ha-list-item-base ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-button.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
ha-list-item-base [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -2,15 +2,14 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/item/ha-row-item";
|
||||
import type {
|
||||
BackupAgent,
|
||||
BackupAgentConfig,
|
||||
@@ -143,10 +142,36 @@ class HaConfigBackupDetails extends LitElement {
|
||||
"ui.panel.config.backup.location.encryption.description"
|
||||
)}
|
||||
</p>
|
||||
<ha-md-list>
|
||||
${CLOUD_AGENT === this.agentId
|
||||
${CLOUD_AGENT === this.agentId
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-button
|
||||
href="https://www.nabucasa.com/config/backups/"
|
||||
target="_blank"
|
||||
slot="end"
|
||||
rel="noreferrer noopener"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`
|
||||
: encrypted
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
@@ -154,82 +179,54 @@ class HaConfigBackupDetails extends LitElement {
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_description"
|
||||
`ui.panel.config.backup.location.encryption.location_encrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
href="https://www.nabucasa.com/config/backups/"
|
||||
target="_blank"
|
||||
slot="end"
|
||||
rel="noreferrer noopener"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._turnOffEncryption}
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted_cloud_learn_more"
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
`
|
||||
: encrypted
|
||||
? html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.location.encryption.location_encrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
slot="end"
|
||||
@click=${this._turnOffEncryption}
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_off"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
|
||||
: html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-row-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_unencrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
slot="end"
|
||||
@click=${this._turnOnEncryption}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_on"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_unencrypted"
|
||||
)}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.location.encryption.location_unencrypted_description`
|
||||
)}
|
||||
</span>
|
||||
|
||||
<ha-button
|
||||
slot="end"
|
||||
@click=${this._turnOnEncryption}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.encryption_turn_on"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
`}
|
||||
</ha-md-list>
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
@@ -336,29 +333,16 @@ class HaConfigBackupDetails extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-list-item-two-line-container-height: 64px;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
ha-row-item img {
|
||||
width: 48px;
|
||||
}
|
||||
ha-md-list-item ha-svg-icon[slot="start"] {
|
||||
ha-row-item ha-svg-icon[slot="start"] {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-md-list.summary ha-md-list-item {
|
||||
--md-list-item-supporting-text-size: 1rem;
|
||||
--md-list-item-label-text-size: 0.875rem;
|
||||
|
||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -371,12 +355,8 @@ class HaConfigBackupDetails extends LitElement {
|
||||
ha-backup-data-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: var(--ha-space-2);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
.dot {
|
||||
display: block;
|
||||
|
||||
@@ -6,8 +6,8 @@ import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
@@ -143,7 +143,7 @@ export class CloudRemotePref extends LitElement {
|
||||
"ui.panel.config.cloud.account.remote.security_options"
|
||||
)}
|
||||
>
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.external_activation"
|
||||
@@ -160,9 +160,9 @@ export class CloudRemotePref extends LitElement {
|
||||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
<hr />
|
||||
<ha-md-list-item>
|
||||
<ha-row-item>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.certificate_info"
|
||||
@@ -194,7 +194,7 @@ export class CloudRemotePref extends LitElement {
|
||||
"ui.panel.config.cloud.account.remote.more_info"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
</ha-expansion-panel>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -281,10 +281,12 @@ export class CloudRemotePref extends LitElement {
|
||||
ha-expansion-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-content-padding: 0 16px;
|
||||
|
||||
@@ -4,9 +4,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/item/ha-row-item";
|
||||
import type { CloudStatusLoggedIn, CloudWebhook } from "../../../../data/cloud";
|
||||
import { createCloudhook, deleteCloudhook } from "../../../../data/cloud";
|
||||
import type { Webhook, WebhookError } from "../../../../data/webhook";
|
||||
@@ -76,7 +76,7 @@ export class CloudWebhooks extends LitElement {
|
||||
`
|
||||
: this._localHooks.map(
|
||||
(entry) => html`
|
||||
<ha-md-list-item .entry=${entry}>
|
||||
<ha-row-item .entry=${entry}>
|
||||
<span slot="headline"
|
||||
>${entry.name}
|
||||
${entry.domain !== entry.name.toLowerCase()
|
||||
@@ -108,7 +108,7 @@ export class CloudWebhooks extends LitElement {
|
||||
@click=${this._enableWebhook}
|
||||
>
|
||||
</ha-switch>`}
|
||||
</ha-md-list-item>
|
||||
</ha-row-item>
|
||||
`
|
||||
)}
|
||||
<div class="footer">
|
||||
@@ -237,12 +237,12 @@ export class CloudWebhooks extends LitElement {
|
||||
.footer a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-md-list-item [slot="supporting-text"] {
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -11,10 +11,14 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { EntitySources } from "../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
HassioSupervisorInfo,
|
||||
@@ -25,9 +29,13 @@ import {
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesParameterized,
|
||||
installUpdates,
|
||||
isSystemUpdate,
|
||||
} from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@@ -35,6 +43,17 @@ import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
|
||||
|
||||
interface UpdateGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
entities: UpdateEntity[];
|
||||
showUpdateAll: boolean;
|
||||
}
|
||||
|
||||
const SYSTEM_KEY = "__system__";
|
||||
const APPS_KEY = "__apps__";
|
||||
const INTEGRATIONS_KEY = "__integrations__";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -47,16 +66,51 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _loadedIntegrationTitles = new Set<string>();
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
this._refreshSupervisorInfo();
|
||||
}
|
||||
|
||||
this._loadEntitySources();
|
||||
}
|
||||
|
||||
private async _loadEntitySources() {
|
||||
try {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
} catch (_err) {
|
||||
// Non-fatal: grouping falls back to entity registry platform lookup.
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
this._loadIntegrationTitles();
|
||||
}
|
||||
|
||||
private async _loadIntegrationTitles() {
|
||||
const domains = new Set<string>();
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
if (!entity.entity_id.startsWith("update.")) continue;
|
||||
const platform = this.hass.entities[entity.entity_id]?.platform;
|
||||
if (platform && !this._loadedIntegrationTitles.has(platform)) {
|
||||
domains.add(platform);
|
||||
}
|
||||
}
|
||||
if (!domains.size) return;
|
||||
const toLoad = Array.from(domains);
|
||||
toLoad.forEach((d) => this._loadedIntegrationTitles.add(d));
|
||||
await this.hass.loadBackendTranslation("title", toLoad);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterInstallableUpdateEntities(
|
||||
const installableUpdates = this._filterInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
@@ -65,6 +119,8 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._showSkipped
|
||||
);
|
||||
|
||||
const groups = this._groupUpdates(installableUpdates, this._entitySources);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
@@ -118,36 +174,52 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${groups.map(
|
||||
(group) => html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: canInstallUpdates.length,
|
||||
})}
|
||||
${group.title}
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
${group.showUpdateAll
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
.group=${group}
|
||||
@click=${this._updateAll}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.update_all"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${group.entities}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
${notInstallableUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: notInstallableUpdates.length,
|
||||
}
|
||||
)}
|
||||
<div class="card-header">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: notInstallableUpdates.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
@@ -159,7 +231,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${canInstallUpdates.length + notInstallableUpdates.length
|
||||
${groups.length + notInstallableUpdates.length
|
||||
? nothing
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
@@ -211,6 +283,22 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private async _updateAll(ev: Event) {
|
||||
const group = (ev.currentTarget as any).group as UpdateGroup;
|
||||
try {
|
||||
await installUpdates(
|
||||
this.hass,
|
||||
group.entities.map((entity) => entity.entity_id)
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.updates.update_all_failed"),
|
||||
text: extractApiErrorMessage(err),
|
||||
warning: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterInstallableUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, false)
|
||||
@@ -221,6 +309,101 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, true)
|
||||
);
|
||||
|
||||
private _groupUpdates = memoizeOne(
|
||||
(
|
||||
entities: UpdateEntity[],
|
||||
entitySources: EntitySources | undefined
|
||||
): UpdateGroup[] => {
|
||||
if (!entities.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const localize = this.hass.localize;
|
||||
|
||||
const systemEntities: UpdateEntity[] = [];
|
||||
const appEntities: UpdateEntity[] = [];
|
||||
const byDomain = new Map<string, UpdateEntity[]>();
|
||||
const otherIntegrationEntities: UpdateEntity[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
if (isSystemUpdate(entity)) {
|
||||
systemEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
const domain =
|
||||
entitySources?.[entity.entity_id]?.domain ??
|
||||
this.hass.entities[entity.entity_id]?.platform;
|
||||
if (domain === "hassio") {
|
||||
appEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
if (!domain) {
|
||||
otherIntegrationEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
if (!byDomain.has(domain)) {
|
||||
byDomain.set(domain, []);
|
||||
}
|
||||
byDomain.get(domain)!.push(entity);
|
||||
}
|
||||
|
||||
const multiInstanceGroups: UpdateGroup[] = [];
|
||||
byDomain.forEach((entries, domain) => {
|
||||
if (entries.length >= 2) {
|
||||
multiInstanceGroups.push({
|
||||
key: domain,
|
||||
title: domainToName(localize, domain),
|
||||
entities: entries,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
} else {
|
||||
otherIntegrationEntities.push(...entries);
|
||||
}
|
||||
});
|
||||
|
||||
multiInstanceGroups.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.title,
|
||||
b.title,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
const groups: UpdateGroup[] = [];
|
||||
|
||||
if (systemEntities.length) {
|
||||
groups.push({
|
||||
key: SYSTEM_KEY,
|
||||
title: localize("ui.panel.config.updates.group_system"),
|
||||
entities: systemEntities,
|
||||
showUpdateAll: false,
|
||||
});
|
||||
}
|
||||
|
||||
groups.push(...multiInstanceGroups);
|
||||
|
||||
if (otherIntegrationEntities.length) {
|
||||
groups.push({
|
||||
key: INTEGRATIONS_KEY,
|
||||
title: localize("ui.panel.config.updates.group_integrations"),
|
||||
entities: otherIntegrationEntities,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (appEntities.length) {
|
||||
groups.push({
|
||||
key: APPS_KEY,
|
||||
title: localize("ui.panel.config.updates.group_apps"),
|
||||
entities: appEntities,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
@@ -247,8 +430,15 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-4) var(--ha-space-2) 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./ha-debug-connection-row";
|
||||
import "./ha-debug-disable-view-transition-row";
|
||||
import "./ha-debug-viewport-environment-card";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/list/ha-list-base";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import { getExtendedEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import {
|
||||
getStatisticMetadata,
|
||||
validateStatistics,
|
||||
} from "../../../../data/recorder";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { getExtendedEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import "./ha-debug-connection-row";
|
||||
import "./ha-debug-disable-view-transition-row";
|
||||
import "./ha-debug-viewport-environment-card";
|
||||
|
||||
@customElement("developer-tools-debug")
|
||||
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
@@ -34,14 +34,14 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.developer-tools.tabs.debug.title"
|
||||
)}
|
||||
>
|
||||
<ha-md-list>
|
||||
<ha-list-base>
|
||||
<ha-debug-connection-row
|
||||
.hass=${this.hass}
|
||||
></ha-debug-connection-row>
|
||||
<ha-debug-disable-view-transition-row
|
||||
.hass=${this.hass}
|
||||
></ha-debug-disable-view-transition-row>
|
||||
</ha-md-list>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
@@ -131,11 +131,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-md-list {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -14,7 +14,7 @@ class HaDebugConnectionRow extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
|
||||
@@ -30,7 +30,7 @@ class HaDebugConnectionRow extends LitElement {
|
||||
.checked=${this.hass.debugConnection}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-debug-disable-view-transition-row")
|
||||
@@ -17,7 +17,7 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<ha-list-item-base>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
|
||||
@@ -33,7 +33,7 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
.checked=${this._disabled}
|
||||
@change=${this._checkedChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,14 @@ import {
|
||||
mdiDownload,
|
||||
mdiMicrophone,
|
||||
mdiOpenInNew,
|
||||
mdiPalette,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRestore,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiShapeOutline,
|
||||
mdiTools,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -29,6 +33,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
@@ -41,8 +46,9 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-nav";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
@@ -89,6 +95,7 @@ import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "../../logbook/ha-logbook";
|
||||
@@ -101,6 +108,51 @@ import {
|
||||
showDeviceRegistryDetailDialog,
|
||||
} from "./device-registry-detail/show-dialog-device-registry-detail";
|
||||
|
||||
type DeviceQuickLinkKey =
|
||||
| "entities"
|
||||
| "helpers"
|
||||
| "automations"
|
||||
| "scenes"
|
||||
| "scripts";
|
||||
|
||||
const NAVIGATION_ACTIONS: {
|
||||
value: string;
|
||||
path: string;
|
||||
icon: string;
|
||||
countKey: DeviceQuickLinkKey;
|
||||
}[] = [
|
||||
{
|
||||
value: "navigate-entities",
|
||||
path: "/config/entities",
|
||||
icon: mdiShapeOutline,
|
||||
countKey: "entities",
|
||||
},
|
||||
{
|
||||
value: "navigate-helpers",
|
||||
path: "/config/helpers",
|
||||
icon: mdiTools,
|
||||
countKey: "helpers",
|
||||
},
|
||||
{
|
||||
value: "navigate-automations",
|
||||
path: "/config/automation/dashboard",
|
||||
icon: mdiRobot,
|
||||
countKey: "automations",
|
||||
},
|
||||
{
|
||||
value: "navigate-scenes",
|
||||
path: "/config/scene/dashboard",
|
||||
icon: mdiPalette,
|
||||
countKey: "scenes",
|
||||
},
|
||||
{
|
||||
value: "navigate-scripts",
|
||||
path: "/config/script/dashboard",
|
||||
icon: mdiScriptText,
|
||||
countKey: "scripts",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
|
||||
stateName?: string | null;
|
||||
}
|
||||
@@ -224,6 +276,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
),
|
||||
}));
|
||||
|
||||
private _getQuickLinkCounts = memoizeOne(
|
||||
(entities: EntityRegistryEntry[], related?: RelatedResult) => ({
|
||||
entities: entities.length,
|
||||
helpers: entities.filter((entity) =>
|
||||
isHelperDomain(computeDomain(entity.entity_id))
|
||||
).length,
|
||||
automations: related?.automation?.length ?? 0,
|
||||
scenes: related?.scene?.length ?? 0,
|
||||
scripts: related?.script?.length ?? 0,
|
||||
})
|
||||
);
|
||||
|
||||
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
|
||||
|
||||
private _entityIds = memoizeOne(
|
||||
@@ -363,6 +427,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this.hass.devices
|
||||
);
|
||||
const entitiesByCategory = this._entitiesByCategory(entities);
|
||||
const quickLinkCounts = this._getQuickLinkCounts(entities, this._related);
|
||||
const batteryEntity = this._batteryEntity(entities);
|
||||
const batteryChargingEntity = this._batteryChargingEntity(entities);
|
||||
const battery = batteryEntity
|
||||
@@ -375,35 +440,42 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: undefined;
|
||||
const area = device.area_id ? this.hass.areas[device.area_id] : undefined;
|
||||
|
||||
const deviceInfo: TemplateResult[] = integrations.map(
|
||||
(integration) =>
|
||||
html`<a
|
||||
slot="actions"
|
||||
href=${`/config/integrations/integration/${integration.domain}#config_entry=${integration.entry_id}`}
|
||||
>
|
||||
<ha-list-item graphic="icon" hasMeta>
|
||||
<img
|
||||
slot="graphic"
|
||||
alt=${domainToName(this.hass.localize, integration.domain)}
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: integration.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
|
||||
${domainToName(this.hass.localize, integration.domain)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
);
|
||||
const deviceInfo: TemplateResult[] = integrations.length
|
||||
? [
|
||||
html`<ha-list-nav slot="actions">
|
||||
${integrations.map(
|
||||
(integration) =>
|
||||
html`<ha-list-item-button
|
||||
href=${`/config/integrations/integration/${integration.domain}#config_entry=${integration.entry_id}`}
|
||||
.headline=${domainToName(
|
||||
this.hass.localize,
|
||||
integration.domain
|
||||
)}
|
||||
>
|
||||
<img
|
||||
slot="start"
|
||||
alt=${domainToName(this.hass.localize, integration.domain)}
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: integration.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
width="24"
|
||||
height="24"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
)}
|
||||
</ha-list-nav>`,
|
||||
]
|
||||
: [];
|
||||
|
||||
const actions = [...(this._deviceActions || [])];
|
||||
if (Array.isArray(this._diagnosticDownloadLinks)) {
|
||||
@@ -502,41 +574,36 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
${this._related.automation?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(
|
||||
this._related
|
||||
).automation.map((automation) =>
|
||||
automation
|
||||
? html`<a
|
||||
href=${ifDefined(
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`
|
||||
)}
|
||||
>
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.automation=${automation}
|
||||
>
|
||||
${computeStateName(automation)}
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item noninteractive>
|
||||
${this.hass.localize(
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${this._related.automation?.length
|
||||
? this._getRelated(
|
||||
this._related
|
||||
).automation.map((automation) =>
|
||||
automation
|
||||
? html`<ha-list-item-button
|
||||
.headline=${computeStateName(
|
||||
automation
|
||||
)}
|
||||
.href=${automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
)
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`}
|
||||
></ha-list-item-base>`}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing}
|
||||
${isComponentLoaded(this.hass.config, "script")
|
||||
@@ -546,12 +613,14 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
</h3>
|
||||
${this._related.script?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(
|
||||
this._related
|
||||
).script.map((script) => {
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${this._related.script?.length
|
||||
? this._getRelated(this._related).script.map(
|
||||
(script) => {
|
||||
if (!script) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -562,28 +631,23 @@ export class HaConfigDevicePage extends LitElement {
|
||||
? `/config/script/edit/${entry.unique_id}`
|
||||
: `/config/script/show/${script.entity_id}`;
|
||||
return html`
|
||||
<a href=${url}>
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.script=${script}
|
||||
>
|
||||
${computeStateName(script)}
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<ha-list-item-button
|
||||
.headline=${computeStateName(script)}
|
||||
.href=${url}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item noninteractive>
|
||||
${this.hass.localize(
|
||||
}
|
||||
)
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`}
|
||||
></ha-list-item-base>`}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing}
|
||||
${hasSceneSupport
|
||||
@@ -593,67 +657,69 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
</h3>
|
||||
${this._related.scene?.length
|
||||
? html`
|
||||
<div class="items">
|
||||
${this._getRelated(this._related).scene.map(
|
||||
(scene) =>
|
||||
scene?.attributes.id
|
||||
? html`
|
||||
<a
|
||||
href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
>
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.scene=${scene}
|
||||
>
|
||||
${computeStateName(scene)}
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item
|
||||
.id="scene-${slugify(
|
||||
scene.entity_id
|
||||
)}"
|
||||
hasMeta
|
||||
.scene=${scene}
|
||||
>
|
||||
${computeStateName(scene)}
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
<ha-tooltip
|
||||
.for="scene-${slugify(
|
||||
scene.entity_id
|
||||
)}"
|
||||
placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item noninteractive>
|
||||
${this.hass.localize(
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${this._related.scene?.length
|
||||
? this._getRelated(this._related).scene.map(
|
||||
(scene) => {
|
||||
if (!scene) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sceneId = `scene-${slugify(
|
||||
scene.entity_id
|
||||
)}`;
|
||||
|
||||
return scene.attributes.id
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
.headline=${computeStateName(
|
||||
scene
|
||||
)}
|
||||
.href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item-base
|
||||
id=${sceneId}
|
||||
.headline=${computeStateName(
|
||||
scene
|
||||
)}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-base>
|
||||
<ha-tooltip
|
||||
.for=${sceneId}
|
||||
placement=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
)
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`}
|
||||
></ha-list-item-base>`}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
@@ -705,16 +771,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<a href=${`/config/entities?historyBack=1&device=${this.deviceId}`}>
|
||||
<ha-dropdown-item>
|
||||
<ha-svg-icon .path=${mdiShapeOutline} slot="icon"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="details"></ha-icon-next>
|
||||
</ha-dropdown-item>
|
||||
</a>
|
||||
${NAVIGATION_ACTIONS.map(
|
||||
(action) => html`
|
||||
<ha-dropdown-item value=${action.value}>
|
||||
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.devices.quick_links.${action.countKey}`,
|
||||
{ count: quickLinkCounts[action.countKey] }
|
||||
)}
|
||||
<ha-icon-next slot="details"></ha-icon-next>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
@@ -1361,6 +1429,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _handleToolbarMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
|
||||
if (navAction) {
|
||||
navigate(`${navAction.path}?historyBack=1&device=${this.deviceId}`);
|
||||
return;
|
||||
}
|
||||
if (action === "reset_entity_ids") {
|
||||
this._resetEntityIds();
|
||||
}
|
||||
@@ -1680,8 +1753,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.items {
|
||||
padding-bottom: var(--ha-space-4);
|
||||
ha-list-item-base ha-icon-next,
|
||||
ha-list-item-button ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-icon-size: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card:has(ha-logbook) {
|
||||
|
||||
@@ -251,12 +251,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (!domain && !configEntry && !label) {
|
||||
if (!area && !domain && !configEntry && !label) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,6 +272,10 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-floor-areas": {
|
||||
value: area ? { areas: [area] } : undefined,
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: domain ? [domain] : [],
|
||||
items: undefined,
|
||||
|
||||
@@ -39,11 +39,15 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _componentLoaded?: boolean;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
private _originalItemJson?: string;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._componentLoaded = isComponentLoaded(
|
||||
@@ -120,7 +124,9 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
@click=${this._updateItem}
|
||||
.disabled=${!!this._submitting || !!(this._item && !this._item.name)}
|
||||
.disabled=${!this._dirty ||
|
||||
!!this._submitting ||
|
||||
!!(this._item && !this._item.name)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
</ha-button>
|
||||
@@ -128,8 +134,18 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _isHelperDirty(): boolean {
|
||||
if (!this._item || !this._originalItemJson) return false;
|
||||
return JSON.stringify(this._item) !== this._originalItemJson;
|
||||
}
|
||||
|
||||
private _updateDirty() {
|
||||
this._dirty = (this._registryEditor?.dirty ?? false) || this._isHelperDirty;
|
||||
}
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._updateDirty();
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
@@ -138,11 +154,15 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
}
|
||||
this._error = undefined;
|
||||
this._item = ev.detail.value;
|
||||
this._updateDirty();
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
|
||||
this._item = items.find((item) => item.id === this.entry.unique_id) || null;
|
||||
this._originalItemJson = this._item
|
||||
? JSON.stringify(this._item)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private async _updateItem(): Promise<void> {
|
||||
|
||||
@@ -208,6 +208,34 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
private _initialStateJson!: string;
|
||||
|
||||
private _lastDirty = false;
|
||||
|
||||
private _currentState() {
|
||||
return {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
entityId: this._entityId.trim(),
|
||||
areaId: this._areaId ?? null,
|
||||
labels: this._labels ?? [],
|
||||
deviceClass: this._deviceClass,
|
||||
disabledBy: this._disabledBy,
|
||||
hiddenBy: this._hiddenBy,
|
||||
unitOfMeasurement: this._unit_of_measurement,
|
||||
precision: this._precision,
|
||||
defaultCode: this._defaultCode,
|
||||
calendarColor: this._calendarColor ?? null,
|
||||
precipitationUnit: this._precipitation_unit,
|
||||
pressureUnit: this._pressure_unit,
|
||||
temperatureUnit: this._temperature_unit,
|
||||
visibilityUnit: this._visibility_unit,
|
||||
windSpeedUnit: this._wind_speed_unit,
|
||||
switchAsDomain: this._switchAsDomain,
|
||||
switchAsInvert: this._switchAsInvert,
|
||||
};
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (
|
||||
@@ -274,6 +302,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
|
||||
}
|
||||
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses || this._hideDeviceClassOverride(domain)) {
|
||||
@@ -372,6 +403,16 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this._switchAsDomain = "switch";
|
||||
this._switchAsInvert = false;
|
||||
}
|
||||
this._initialStateJson = JSON.stringify(this._currentState());
|
||||
this._lastDirty = false;
|
||||
}
|
||||
|
||||
if (this._initialStateJson) {
|
||||
const dirty = this.dirty;
|
||||
if (dirty !== this._lastDirty) {
|
||||
this._lastDirty = dirty;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +448,23 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@input=${this._nameChanged}
|
||||
>
|
||||
${this._device
|
||||
? html`<span slot="hint"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_name_tip",
|
||||
{
|
||||
link: html`<button
|
||||
class="link"
|
||||
@click=${this._resetNameAndOpenDeviceSettings}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.open_device_settings"
|
||||
)}
|
||||
</button>`,
|
||||
}
|
||||
)}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-input>`}
|
||||
${this.hideIcon
|
||||
? nothing
|
||||
@@ -1060,6 +1118,10 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public get dirty(): boolean {
|
||||
return JSON.stringify(this._currentState()) !== this._initialStateJson;
|
||||
}
|
||||
|
||||
public async updateEntry(): Promise<{
|
||||
close: boolean;
|
||||
entry: ExtEntityRegistryEntry;
|
||||
@@ -1518,6 +1580,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _resetNameAndOpenDeviceSettings() {
|
||||
this._name = this.entry.name || "";
|
||||
fireEvent(this, "change");
|
||||
|
||||
this._openDeviceSettings();
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this._device!,
|
||||
|
||||
@@ -44,6 +44,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@query("entity-registry-settings-editor")
|
||||
private _registryEditor?: EntityRegistrySettingsEditor;
|
||||
|
||||
@@ -144,7 +146,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._updateEntry} .loading=${!!this._submitting}>
|
||||
<ha-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._dirty || !!this._submitting}
|
||||
.loading=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.update")}
|
||||
</ha-button>
|
||||
</div>
|
||||
@@ -153,6 +159,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _entityRegistryChanged() {
|
||||
this._error = undefined;
|
||||
this._dirty = this._registryEditor?.dirty ?? false;
|
||||
}
|
||||
|
||||
private _openDeviceSettings() {
|
||||
|
||||
@@ -1092,6 +1092,7 @@ export class HaConfigEntities extends LitElement {
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
@@ -1099,7 +1100,7 @@ export class HaConfigEntities extends LitElement {
|
||||
const label = this._searchParms.get("label");
|
||||
const voiceAssistant = this._searchParms.get("voice_assistant");
|
||||
|
||||
if (!domain && !configEntry && !label && !device) {
|
||||
if (!area && !domain && !configEntry && !label && !device) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1108,6 +1109,7 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
|
||||
@@ -625,7 +625,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -964,12 +966,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.get("label");
|
||||
const category = this._searchParms.get("category");
|
||||
const voiceAssistant = this._searchParms.get("voice_assistant");
|
||||
|
||||
if (!category && !label && !device) {
|
||||
if (!area && !category && !label && !device && !voiceAssistant) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -977,6 +980,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
this._filter = history.state?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
"ha-filter-categories": category ? [category] : [],
|
||||
|
||||
+9
-3
@@ -19,6 +19,7 @@ import type {
|
||||
HaScannerType,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import {
|
||||
isScannerStateMismatch,
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
subscribeBluetoothScannersDetails,
|
||||
@@ -285,9 +286,7 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
const isRemoteScanner = scannerType === "remote";
|
||||
const hasMismatch =
|
||||
scannerState &&
|
||||
scannerState.current_mode !== scannerState.requested_mode;
|
||||
const hasMismatch = scannerState && isScannerStateMismatch(scannerState);
|
||||
|
||||
const allocations = scannerDetails
|
||||
? this._connectionAllocationData.find(
|
||||
@@ -438,6 +437,13 @@ export class BluetoothAdapterInfoPage extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (scannerState.requested_mode === "auto") {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_auto_with_current",
|
||||
{ current: this._formatMode(scannerState.current_mode) }
|
||||
);
|
||||
}
|
||||
|
||||
return this._formatModeLabel(scannerState.current_mode);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import type {
|
||||
BluetoothScannerState,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import {
|
||||
isScannerStateMismatch,
|
||||
subscribeBluetoothAdvertisements,
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
@@ -144,7 +145,7 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
0
|
||||
);
|
||||
const hasMismatch = Object.values(this._scannerStates).some(
|
||||
(s) => s.current_mode !== s.requested_mode
|
||||
isScannerStateMismatch
|
||||
);
|
||||
const isOffline = adapterCount === 0;
|
||||
const status = isOffline ? "offline" : hasMismatch ? "warning" : "online";
|
||||
|
||||
+23
-2
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
|
||||
@@ -121,6 +121,19 @@ class DialogBluetoothDeviceInfo extends LitElement {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
${this._params.entry.raw
|
||||
? html`
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.raw_advertisement"
|
||||
)}
|
||||
</h4>
|
||||
<div class="raw">
|
||||
${this.showDataAsHex(this._params.entry.raw)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -133,6 +146,14 @@ class DialogBluetoothDeviceInfo extends LitElement {
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
.raw {
|
||||
word-break: break-all;
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../../components/list/types";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
|
||||
import {
|
||||
addMembersToGroup,
|
||||
fetchGroup,
|
||||
fetchGroupableDevices,
|
||||
} from "../../../../../data/zha";
|
||||
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleScrollbar } from "../../../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZHAAddGroupMembersDialogParams } from "./show-dialog-zha-add-group-members";
|
||||
|
||||
@customElement("dialog-zha-add-group-members")
|
||||
class DialogZHAAddGroupMembers
|
||||
extends LitElement
|
||||
implements HassDialog<ZHAAddGroupMembersDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _deviceEndpoints: ZHADeviceEndpoint[] = [];
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _group?: ZHAGroup;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _params?: ZHAAddGroupMembersDialogParams;
|
||||
|
||||
@state() private _processingAdd = false;
|
||||
|
||||
@state() private _selectedDevicesToAdd: string[] = [];
|
||||
|
||||
@state() private _virtualizerReady = false;
|
||||
|
||||
private _fetchDataToken = 0;
|
||||
|
||||
public showDialog(params: ZHAAddGroupMembersDialogParams): void {
|
||||
this._params = params;
|
||||
this._deviceEndpoints = [];
|
||||
this._filter = "";
|
||||
this._group = undefined;
|
||||
this._selectedDevicesToAdd = [];
|
||||
this._open = true;
|
||||
this._fetchData();
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
if (this._processingAdd) {
|
||||
return false;
|
||||
}
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._deviceEndpoints = [];
|
||||
this._filter = "";
|
||||
this._group = undefined;
|
||||
this._loading = false;
|
||||
this._processingAdd = false;
|
||||
this._selectedDevicesToAdd = [];
|
||||
this._virtualizerReady = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceEndpoints = this._filteredDeviceEndpoints;
|
||||
const showSearch =
|
||||
this._availableDeviceEndpoints.length > 5 || this._filter;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.add_members"
|
||||
)}
|
||||
?prevent-scrim-close=${this._selectedDevicesToAdd.length > 0}
|
||||
@after-show=${this._loadVirtualizer}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
.disabled=${this._processingAdd}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<div class="content">
|
||||
${this._loading
|
||||
? this._renderLoadingSpinner()
|
||||
: html`
|
||||
${showSearch
|
||||
? html`
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleFilterChanged}
|
||||
></ha-input-search>
|
||||
`
|
||||
: nothing}
|
||||
<div class="list-container">
|
||||
${deviceEndpoints.length
|
||||
? html`
|
||||
${this._virtualizerReady
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
multi
|
||||
@ha-list-selected=${this._handleSelected}
|
||||
>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${deviceEndpoints}
|
||||
.renderItem=${this._renderDeviceEndpoint}
|
||||
.keyFunction=${this._keyFunction}
|
||||
></lit-virtualizer>
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: this._renderLoadingSpinner()}
|
||||
`
|
||||
: html`
|
||||
<div class="empty-list">
|
||||
${this._filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.zha.groups.no_devices_found"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.zha.groups.no_devices_to_add"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._processingAdd}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._loading ||
|
||||
!this._selectedDevicesToAdd.length ||
|
||||
this._processingAdd}
|
||||
.loading=${this._processingAdd}
|
||||
@click=${this._addMembersToGroup}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLoadingSpinner(): TemplateResult {
|
||||
return html`
|
||||
<div class="spinner-container">
|
||||
<ha-spinner size="medium"></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _availableDeviceEndpoints(): ZHADeviceEndpoint[] {
|
||||
if (!this._group) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this._deviceEndpoints.filter(
|
||||
(deviceEndpoint) =>
|
||||
!this._group!.members.some(
|
||||
(member) =>
|
||||
member.device.ieee === deviceEndpoint.device.ieee &&
|
||||
member.endpoint_id === deviceEndpoint.endpoint_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private get _filteredDeviceEndpoints(): ZHADeviceEndpoint[] {
|
||||
const normalizedFilter = this._filter.trim().toLowerCase();
|
||||
const deviceEndpoints = this._availableDeviceEndpoints;
|
||||
|
||||
if (!normalizedFilter) {
|
||||
return deviceEndpoints;
|
||||
}
|
||||
|
||||
return deviceEndpoints.filter((deviceEndpoint) =>
|
||||
[
|
||||
this._deviceEndpointName(deviceEndpoint),
|
||||
this._deviceEndpointDetails(deviceEndpoint),
|
||||
deviceEndpoint.device.ieee,
|
||||
deviceEndpoint.device.manufacturer,
|
||||
deviceEndpoint.device.model,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(normalizedFilter))
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadVirtualizer(): Promise<void> {
|
||||
await loadVirtualizer();
|
||||
this._virtualizerReady = true;
|
||||
}
|
||||
|
||||
private _keyFunction = (deviceEndpoint: unknown): string =>
|
||||
this._deviceEndpointId(deviceEndpoint as ZHADeviceEndpoint);
|
||||
|
||||
private _renderDeviceEndpoint: RenderItemFunction<ZHADeviceEndpoint> = (
|
||||
deviceEndpoint
|
||||
) => {
|
||||
const id = this._deviceEndpointId(deviceEndpoint);
|
||||
|
||||
return html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
.value=${id}
|
||||
.selected=${this._selectedDevicesToAdd.includes(id)}
|
||||
>
|
||||
<span slot="headline">${this._deviceEndpointName(deviceEndpoint)}</span>
|
||||
<span slot="supporting-text">
|
||||
${this._deviceEndpointDetails(deviceEndpoint)}
|
||||
</span>
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
};
|
||||
|
||||
private _deviceEndpointId(deviceEndpoint: ZHADeviceEndpoint): string {
|
||||
return `${deviceEndpoint.device.ieee}_${deviceEndpoint.endpoint_id}`;
|
||||
}
|
||||
|
||||
private _deviceEndpointName(deviceEndpoint: ZHADeviceEndpoint): string {
|
||||
return deviceEndpoint.device.user_given_name || deviceEndpoint.device.name;
|
||||
}
|
||||
|
||||
private _deviceEndpointDetails(deviceEndpoint: ZHADeviceEndpoint): string {
|
||||
const entityNames = deviceEndpoint.entities.map(
|
||||
(entity) => entity.name || entity.original_name || entity.entity_id
|
||||
);
|
||||
const entitySummary = entityNames.length
|
||||
? entityNames.length > 2
|
||||
? `${entityNames.slice(0, 2).join(", ")} +${entityNames.length - 2}`
|
||||
: entityNames.join(", ")
|
||||
: this.hass.localize("ui.panel.config.zha.groups.no_entities");
|
||||
|
||||
return [
|
||||
deviceEndpoint.device.area_id
|
||||
? this.hass.areas[deviceEndpoint.device.area_id]?.name
|
||||
: undefined,
|
||||
`${this.hass.localize("ui.panel.config.zha.groups.endpoint")} ${
|
||||
deviceEndpoint.endpoint_id
|
||||
}`,
|
||||
entitySummary,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
private async _fetchData(): Promise<void> {
|
||||
const token = ++this._fetchDataToken;
|
||||
this._loading = true;
|
||||
const [group, deviceEndpoints] = await Promise.all([
|
||||
fetchGroup(this.hass, this._params!.groupId),
|
||||
fetchGroupableDevices(this.hass),
|
||||
]);
|
||||
|
||||
if (token !== this._fetchDataToken || !this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._group = group;
|
||||
this._deviceEndpoints = deviceEndpoints;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
private _handleFilterChanged(ev: Event): void {
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleSelected(ev: CustomEvent<HaListSelectedDetail>): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDevicesToAdd = this._selectedDevicesToAdd;
|
||||
|
||||
ev.detail.diff?.added.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value && !selectedDevicesToAdd.includes(item.value)) {
|
||||
selectedDevicesToAdd = [...selectedDevicesToAdd, item.value];
|
||||
}
|
||||
});
|
||||
|
||||
ev.detail.diff?.removed.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDevicesToAdd = selectedDevicesToAdd.filter(
|
||||
(selectedDeviceId) => selectedDeviceId !== item.value
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._selectedDevicesToAdd = selectedDevicesToAdd;
|
||||
}
|
||||
|
||||
private async _addMembersToGroup(): Promise<void> {
|
||||
this._processingAdd = true;
|
||||
try {
|
||||
const members = this._selectedDevicesToAdd.map((member) => {
|
||||
const memberParts = member.split("_");
|
||||
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
|
||||
});
|
||||
const group = await addMembersToGroup(
|
||||
this.hass,
|
||||
this._params!.groupId,
|
||||
members
|
||||
);
|
||||
this._params!.devicesAddedCallback(group);
|
||||
this._processingAdd = false;
|
||||
this.closeDialog();
|
||||
} finally {
|
||||
this._processingAdd = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: min(520px, calc(100vh - 240px));
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-list-selectable {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-list-selectable::part(base) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
contain: size layout !important;
|
||||
}
|
||||
|
||||
ha-list-item-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
box-sizing: border-box;
|
||||
--ha-row-item-min-height: 64px;
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
ha-spinner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
padding: var(--ha-space-6);
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-zha-add-group-members": DialogZHAAddGroupMembers;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { ZHAGroup } from "../../../../../data/zha";
|
||||
|
||||
export interface ZHAAddGroupMembersDialogParams {
|
||||
groupId: number;
|
||||
groupName: string;
|
||||
devicesAddedCallback: (group: ZHAGroup) => void;
|
||||
}
|
||||
|
||||
export const loadZHAAddGroupMembersDialog = () =>
|
||||
import("./dialog-zha-add-group-members");
|
||||
|
||||
export const showZHAAddGroupMembersDialog = (
|
||||
element: HTMLElement,
|
||||
params: ZHAAddGroupMembersDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-zha-add-group-members",
|
||||
dialogImport: loadZHAAddGroupMembersDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -3,16 +3,18 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
|
||||
import { addGroup, fetchGroupableDevices } from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import "./zha-device-endpoint-data-table";
|
||||
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
|
||||
@customElement("zha-add-group-page")
|
||||
export class ZHAAddGroupPage extends LitElement {
|
||||
@@ -29,8 +31,8 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
|
||||
@state() private _groupId?: string;
|
||||
|
||||
@query("zha-device-endpoint-data-table", true)
|
||||
private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable;
|
||||
@query("zha-device-endpoint-list", true)
|
||||
private _zhaDeviceEndpointList!: ZHADeviceEndpointList;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
@@ -57,59 +59,67 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.zha.groups.create_group")}
|
||||
back-path="/config/zha/groups"
|
||||
>
|
||||
<ha-config-section .isWide=${!this.narrow}>
|
||||
<p slot="introduction">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.create_group_details"
|
||||
)}
|
||||
</p>
|
||||
<ha-input
|
||||
type="string"
|
||||
.value=${this._groupName}
|
||||
@change=${this._handleNameChange}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.group_name_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
<div class="container">
|
||||
<ha-card class="details-card">
|
||||
<div class="card-header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<ha-input
|
||||
type="text"
|
||||
.value=${this._groupName}
|
||||
@change=${this._handleNameChange}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.group_name_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
|
||||
<ha-input
|
||||
type="number"
|
||||
.value=${this._groupId}
|
||||
@change=${this._handleGroupIdChange}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.group_id_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
<ha-input
|
||||
type="number"
|
||||
.value=${this._groupId}
|
||||
@change=${this._handleGroupIdChange}
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.group_id_placeholder"
|
||||
)}
|
||||
></ha-input>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<div class="header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
|
||||
</div>
|
||||
<section>
|
||||
<h2>
|
||||
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
|
||||
</h2>
|
||||
|
||||
<zha-device-endpoint-data-table
|
||||
.hass=${this.hass}
|
||||
.deviceEndpoints=${this.deviceEndpoints}
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleAddSelectionChanged}
|
||||
>
|
||||
</zha-device-endpoint-data-table>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
.disabled=${!this._groupName ||
|
||||
this._groupName === "" ||
|
||||
this._processingAdd}
|
||||
@click=${this._createGroup}
|
||||
class="button"
|
||||
.loading=${this._processingAdd}
|
||||
<zha-device-endpoint-list
|
||||
scrollable
|
||||
show-device-link
|
||||
.deviceEndpoints=${this.deviceEndpoints}
|
||||
.narrow=${this.narrow}
|
||||
.emptyText=${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.no_devices_to_add"
|
||||
)}
|
||||
selectable
|
||||
@selection-changed=${this._handleAddSelectionChanged}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.create"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-config-section>
|
||||
</zha-device-endpoint-list>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
.disabled=${!this._groupName ||
|
||||
this._groupName === "" ||
|
||||
this._processingAdd}
|
||||
@click=${this._createGroup}
|
||||
.loading=${this._processingAdd}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.create"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -119,7 +129,7 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
@@ -142,7 +152,7 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
this._selectedDevicesToAdd = [];
|
||||
this._processingAdd = false;
|
||||
this._groupName = "";
|
||||
this._zhaDevicesDataTable.clearSelection();
|
||||
this._zhaDeviceEndpointList.clearSelection();
|
||||
navigate(`/config/zha/group/${group.group_id}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -157,29 +167,54 @@ export class ZHAAddGroupPage extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
.header {
|
||||
font-family: var(--ha-font-family-body);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
font-size: var(--ha-font-size-4xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4) var(--ha-space-4)
|
||||
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.button {
|
||||
float: right;
|
||||
.card-content {
|
||||
display: grid;
|
||||
gap: var(--ha-space-4);
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-config-section *:last-child {
|
||||
padding-bottom: 24px;
|
||||
section {
|
||||
margin-top: var(--ha-space-8);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--ha-space-3);
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
zha-device-endpoint-list {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
align-items: flex-end;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--ha-space-4) 0 0;
|
||||
}
|
||||
.buttons .warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding-inline: var(--ha-space-2);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
HaDataTable,
|
||||
} from "../../../../../components/data-table/ha-data-table";
|
||||
import type {
|
||||
ZHADeviceEndpoint,
|
||||
ZHAEntityReference,
|
||||
} from "../../../../../data/zha";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { getAreaTableColumn } from "../../../common/data-table-columns";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area/area_registry";
|
||||
|
||||
export interface DeviceEndpointRowData extends DataTableRowData {
|
||||
id: string;
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
model: string;
|
||||
manufacturer: string;
|
||||
endpoint_id: number;
|
||||
entities: ZHAEntityReference[];
|
||||
}
|
||||
|
||||
@customElement("zha-device-endpoint-data-table")
|
||||
export class ZHADeviceEndpointDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceEndpoints: ZHADeviceEndpoint[] = [];
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
private _deviceEndpoints = memoizeOne(
|
||||
(
|
||||
deviceEndpoints: ZHADeviceEndpoint[],
|
||||
areas: Record<string, AreaRegistryEntry>
|
||||
) => {
|
||||
const outputDevices: DeviceEndpointRowData[] = [];
|
||||
deviceEndpoints.forEach((deviceEndpoint) => {
|
||||
outputDevices.push({
|
||||
name:
|
||||
deviceEndpoint.device.user_given_name || deviceEndpoint.device.name,
|
||||
area: deviceEndpoint.device.area_id
|
||||
? areas[deviceEndpoint.device.area_id].name
|
||||
: undefined,
|
||||
model: deviceEndpoint.device.model,
|
||||
manufacturer: deviceEndpoint.device.manufacturer,
|
||||
id: deviceEndpoint.device.ieee + "_" + deviceEndpoint.endpoint_id,
|
||||
ieee: deviceEndpoint.device.ieee,
|
||||
endpoint_id: deviceEndpoint.endpoint_id,
|
||||
entities: deviceEndpoint.entities,
|
||||
dev_id: deviceEndpoint.device.device_reg_id,
|
||||
});
|
||||
});
|
||||
|
||||
return outputDevices;
|
||||
}
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc, narrow: boolean): DataTableColumnContainer =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: localize("ui.panel.config.zha.groups.members"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
template: (device) => html`
|
||||
<a href=${`/config/devices/device/${device.dev_id}`}>
|
||||
${device.name}
|
||||
${device.area
|
||||
? html` <br />
|
||||
<span
|
||||
style="font-size: var(--ha-font-size-s);color: var(--ha-color-text-secondary);"
|
||||
>
|
||||
${device.area}
|
||||
</span>`
|
||||
: nothing}
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
endpoint_id: {
|
||||
title: localize("ui.panel.config.zha.groups.endpoint"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: localize("ui.panel.config.zha.groups.members"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
template: (device) => html`
|
||||
<a href=${`/config/devices/device/${device.dev_id}`}>
|
||||
${device.name}
|
||||
</a>
|
||||
`,
|
||||
},
|
||||
area: getAreaTableColumn(localize),
|
||||
endpoint_id: {
|
||||
title: localize("ui.panel.config.zha.groups.endpoint"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
entities: {
|
||||
title: localize("ui.panel.config.zha.groups.associated_entities"),
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
flex: 2,
|
||||
template: (device) => html`
|
||||
${device.entities.length
|
||||
? device.entities.length > 3
|
||||
? html`${device.entities
|
||||
.slice(0, 2)
|
||||
.map(
|
||||
(entity) =>
|
||||
html`<div
|
||||
style="overflow: hidden; text-overflow: ellipsis;"
|
||||
>
|
||||
${entity.name || entity.original_name}
|
||||
</div>`
|
||||
)}
|
||||
<div>+${device.entities.length - 2}</div>`
|
||||
: device.entities.map(
|
||||
(entity) =>
|
||||
html`<div
|
||||
style="overflow: hidden; text-overflow: ellipsis;"
|
||||
>
|
||||
${entity.name || entity.original_name}
|
||||
</div>`
|
||||
)
|
||||
: localize(
|
||||
"ui.panel.config.zha.groups.no_associated_entities"
|
||||
)}
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
public clearSelection() {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-data-table
|
||||
.columns=${this._columns(this.hass.localize, this.narrow)}
|
||||
.data=${this._deviceEndpoints(this.deviceEndpoints, this.hass.areas)}
|
||||
.selectable=${this.selectable}
|
||||
auto-height
|
||||
.searchLabel=${this.hass.localize("ui.components.data-table.search")}
|
||||
.noDataText=${this.hass.localize("ui.components.data-table.no-data")}
|
||||
></ha-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
.table-cell-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-device-endpoint-data-table": ZHADeviceEndpointDataTable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import "../../../../../components/item/ha-list-item-base";
|
||||
import "../../../../../components/item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
|
||||
import "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
|
||||
import type { HaListSelectedDetail } from "../../../../../components/list/types";
|
||||
import {
|
||||
areasContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../../data/context";
|
||||
import type {
|
||||
ZHADeviceEndpoint,
|
||||
ZHAEntityReference,
|
||||
} from "../../../../../data/zha";
|
||||
|
||||
export interface DeviceEndpointRowData {
|
||||
id: string;
|
||||
name: string;
|
||||
area: string | undefined;
|
||||
model: string;
|
||||
manufacturer: string;
|
||||
endpoint_id: number;
|
||||
entities: ZHAEntityReference[];
|
||||
ieee: string;
|
||||
dev_id: string;
|
||||
}
|
||||
|
||||
export interface DeviceEndpointSelectionChangedEvent {
|
||||
value: string[];
|
||||
}
|
||||
|
||||
@customElement("zha-device-endpoint-list")
|
||||
export class ZHADeviceEndpointList extends LitElement {
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
|
||||
@property({ type: Boolean }) public scrollable = false;
|
||||
|
||||
@property({ attribute: false }) public emptyText?: string;
|
||||
|
||||
@property({ attribute: "show-device-link", type: Boolean })
|
||||
public showDeviceLink = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceEndpoints: ZHADeviceEndpoint[] = [];
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _selectedDeviceIds: string[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@query("ha-list-selectable") private _list?: HaListSelectable;
|
||||
|
||||
public clearSelection() {
|
||||
this._selectedDeviceIds = [];
|
||||
this._list?.clearSelection();
|
||||
this._fireSelectionChanged();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const allDeviceEndpoints = this._deviceEndpointRows;
|
||||
const deviceEndpoints = this._filterDeviceEndpoints(allDeviceEndpoints);
|
||||
const showSearch = allDeviceEndpoints.length > 5 || this._filter;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${`${showSearch ? "searchable" : ""} ${
|
||||
this.scrollable ? "scrollable" : ""
|
||||
}`}
|
||||
>
|
||||
${showSearch
|
||||
? html`
|
||||
<div class="search">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleFilterChanged}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${deviceEndpoints.length
|
||||
? html`
|
||||
${this.selectable
|
||||
? html`
|
||||
<ha-list-selectable
|
||||
multi
|
||||
@ha-list-selected=${this._handleListSelectionChanged}
|
||||
>
|
||||
${repeat(
|
||||
deviceEndpoints,
|
||||
(deviceEndpoint) => deviceEndpoint.id,
|
||||
(deviceEndpoint) =>
|
||||
this._renderSelectableListRow(deviceEndpoint)
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
${repeat(
|
||||
deviceEndpoints,
|
||||
(deviceEndpoint) => deviceEndpoint.id,
|
||||
(deviceEndpoint) =>
|
||||
this._renderReadonlyListRow(deviceEndpoint)
|
||||
)}
|
||||
</ha-list>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<div class="empty-list">
|
||||
${this._filter
|
||||
? this._i18n.localize(
|
||||
"ui.panel.config.zha.groups.no_devices_found"
|
||||
)
|
||||
: this.emptyText ||
|
||||
this._i18n.localize("ui.components.data-table.no-data")}
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _deviceEndpointRows(): DeviceEndpointRowData[] {
|
||||
return this.deviceEndpoints.map((deviceEndpoint) => ({
|
||||
name: deviceEndpoint.device.user_given_name || deviceEndpoint.device.name,
|
||||
area: deviceEndpoint.device.area_id
|
||||
? this._areas[deviceEndpoint.device.area_id]?.name
|
||||
: undefined,
|
||||
model: deviceEndpoint.device.model,
|
||||
manufacturer: deviceEndpoint.device.manufacturer,
|
||||
id: `${deviceEndpoint.device.ieee}_${deviceEndpoint.endpoint_id}`,
|
||||
ieee: deviceEndpoint.device.ieee,
|
||||
endpoint_id: deviceEndpoint.endpoint_id,
|
||||
entities: deviceEndpoint.entities,
|
||||
dev_id: deviceEndpoint.device.device_reg_id,
|
||||
}));
|
||||
}
|
||||
|
||||
private _renderSelectableListRow(
|
||||
deviceEndpoint: DeviceEndpointRowData
|
||||
): TemplateResult {
|
||||
const selected = this._selectedDeviceIds.includes(deviceEndpoint.id);
|
||||
|
||||
return html`
|
||||
<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
class="device-row"
|
||||
.value=${deviceEndpoint.id}
|
||||
.selected=${selected}
|
||||
>
|
||||
<span slot="headline">${deviceEndpoint.name}</span>
|
||||
<span slot="supporting-text">
|
||||
${this._deviceEndpointDetails(deviceEndpoint)}
|
||||
</span>
|
||||
${this.showDeviceLink
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiOpenInNew}
|
||||
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.zha.groups.open_device"
|
||||
)}
|
||||
@click=${this._stopPropagation}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item-option>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderReadonlyListRow(
|
||||
deviceEndpoint: DeviceEndpointRowData
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<ha-list-item-base class="device-row">
|
||||
<span slot="headline">${deviceEndpoint.name}</span>
|
||||
<span slot="supporting-text">
|
||||
${this._deviceEndpointDetails(deviceEndpoint)}
|
||||
</span>
|
||||
${this.showDeviceLink
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiOpenInNew}
|
||||
.href=${`/config/devices/device/${deviceEndpoint.dev_id}`}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.zha.groups.open_device"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterDeviceEndpoints(
|
||||
deviceEndpoints: DeviceEndpointRowData[]
|
||||
): DeviceEndpointRowData[] {
|
||||
const normalizedFilter = this._filter.trim().toLowerCase();
|
||||
|
||||
if (!normalizedFilter) {
|
||||
return deviceEndpoints;
|
||||
}
|
||||
|
||||
return deviceEndpoints.filter((deviceEndpoint) =>
|
||||
[
|
||||
deviceEndpoint.name,
|
||||
this._deviceEndpointDetails(deviceEndpoint),
|
||||
deviceEndpoint.ieee,
|
||||
deviceEndpoint.manufacturer,
|
||||
deviceEndpoint.model,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => value!.toLowerCase().includes(normalizedFilter))
|
||||
);
|
||||
}
|
||||
|
||||
private _deviceEndpointDetails(
|
||||
deviceEndpoint: DeviceEndpointRowData
|
||||
): string {
|
||||
const entityNames = deviceEndpoint.entities.map(
|
||||
(entity) => entity.name || entity.original_name || entity.entity_id
|
||||
);
|
||||
const entitySummary = entityNames.length
|
||||
? entityNames.length > 2
|
||||
? `${entityNames.slice(0, 2).join(", ")} +${entityNames.length - 2}`
|
||||
: entityNames.join(", ")
|
||||
: this._i18n.localize("ui.panel.config.zha.groups.no_entities");
|
||||
|
||||
return [
|
||||
deviceEndpoint.area,
|
||||
`${this._i18n.localize("ui.panel.config.zha.groups.endpoint")} ${
|
||||
deviceEndpoint.endpoint_id
|
||||
}`,
|
||||
entitySummary,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
private _handleFilterChanged(ev: Event): void {
|
||||
this._filter = (ev.currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private _handleListSelectionChanged(
|
||||
ev: CustomEvent<HaListSelectedDetail>
|
||||
): void {
|
||||
const list = ev.currentTarget as HaListSelectable;
|
||||
let selectedDeviceIds = this._selectedDeviceIds;
|
||||
|
||||
ev.detail.diff?.added.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ev.detail.diff?.removed.forEach((index) => {
|
||||
const item = list.items[index] as HaListItemOption | undefined;
|
||||
if (item?.value) {
|
||||
selectedDeviceIds = this._setSelectedDeviceId(
|
||||
selectedDeviceIds,
|
||||
item.value,
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._selectedDeviceIds = selectedDeviceIds;
|
||||
this._fireSelectionChanged();
|
||||
}
|
||||
|
||||
private _setSelectedDeviceId(
|
||||
selectedDeviceIds: string[],
|
||||
deviceId: string,
|
||||
selected: boolean
|
||||
): string[] {
|
||||
if (selected) {
|
||||
return selectedDeviceIds.includes(deviceId)
|
||||
? selectedDeviceIds
|
||||
: [...selectedDeviceIds, deviceId];
|
||||
}
|
||||
|
||||
return selectedDeviceIds.filter((selectedDeviceId) => {
|
||||
return selectedDeviceId !== deviceId;
|
||||
});
|
||||
}
|
||||
|
||||
private _fireSelectionChanged(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<DeviceEndpointSelectionChangedEvent>(
|
||||
"selection-changed",
|
||||
{
|
||||
detail: { value: this._selectedDeviceIds },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _stopPropagation(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-card.scrollable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-card.searchable.scrollable {
|
||||
height: min(520px, calc(100vh - 360px));
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
|
||||
ha-list,
|
||||
ha-list-selectable {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ha-list-selectable::part(base) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ha-card.scrollable ha-list,
|
||||
ha-card.scrollable ha-list-selectable {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.device-row {
|
||||
width: 100%;
|
||||
--ha-row-item-min-height: 64px;
|
||||
--ha-row-item-gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
[slot="headline"],
|
||||
[slot="supporting-text"] {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
padding: var(--ha-space-6);
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
ha-card.searchable.scrollable {
|
||||
height: 440px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zha-device-endpoint-list": ZHADeviceEndpointList;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import type { ZHADeviceEndpoint, ZHAGroup } from "../../../../../data/zha";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { ZHAGroup } from "../../../../../data/zha";
|
||||
import {
|
||||
addMembersToGroup,
|
||||
fetchGroup,
|
||||
fetchGroupableDevices,
|
||||
removeGroups,
|
||||
removeMembersFromGroup,
|
||||
} from "../../../../../data/zha";
|
||||
import "../../../../../layouts/hass-error-screen";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../ha-config-section";
|
||||
import { formatAsPaddedHex } from "./functions";
|
||||
import "./zha-device-endpoint-data-table";
|
||||
import type { ZHADeviceEndpointDataTable } from "./zha-device-endpoint-data-table";
|
||||
import "./zha-device-endpoint-list";
|
||||
import type {
|
||||
DeviceEndpointSelectionChangedEvent,
|
||||
ZHADeviceEndpointList,
|
||||
} from "./zha-device-endpoint-list";
|
||||
import { showZHAAddGroupMembersDialog } from "./show-dialog-zha-add-group-members";
|
||||
|
||||
@customElement("zha-group-page")
|
||||
export class ZHAGroupPage extends LitElement {
|
||||
@@ -38,25 +37,12 @@ export class ZHAGroupPage extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceEndpoints: ZHADeviceEndpoint[] = [];
|
||||
|
||||
@state() private _processingAdd = false;
|
||||
|
||||
@state() private _processingRemove = false;
|
||||
|
||||
@state()
|
||||
private _filteredDeviceEndpoints: ZHADeviceEndpoint[] = [];
|
||||
|
||||
@state() private _selectedDevicesToAdd: string[] = [];
|
||||
|
||||
@state() private _selectedDevicesToRemove: string[] = [];
|
||||
|
||||
@query("#addMembers", true)
|
||||
private _zhaAddMembersDataTable!: ZHADeviceEndpointDataTable;
|
||||
|
||||
@query("#removeMembers")
|
||||
private _zhaRemoveMembersDataTable!: ZHADeviceEndpointDataTable;
|
||||
private _zhaRemoveMembersList!: ZHADeviceEndpointList;
|
||||
|
||||
private _firstUpdatedCalled = false;
|
||||
|
||||
@@ -69,12 +55,8 @@ export class ZHAGroupPage extends LitElement {
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._processingAdd = false;
|
||||
this._processingRemove = false;
|
||||
this._selectedDevicesToRemove = [];
|
||||
this._selectedDevicesToAdd = [];
|
||||
this.deviceEndpoints = [];
|
||||
this._filteredDeviceEndpoints = [];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
@@ -102,6 +84,7 @@ export class ZHAGroupPage extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.group.name}
|
||||
back-path="/config/zha/groups"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@@ -109,158 +92,115 @@ export class ZHAGroupPage extends LitElement {
|
||||
@click=${this._deleteGroup}
|
||||
.label=${this.hass.localize("ui.panel.config.zha.groups.delete")}
|
||||
></ha-icon-button>
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<div class="header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
|
||||
</div>
|
||||
|
||||
<p slot="introduction">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.group_details")}
|
||||
</p>
|
||||
|
||||
<p><b>Name:</b> ${this.group.name}</p>
|
||||
<p><b>Group Id:</b> ${formatAsPaddedHex(this.group.group_id)}</p>
|
||||
|
||||
<div class="header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.members")}
|
||||
</div>
|
||||
<div class="container">
|
||||
<ha-card>
|
||||
<ha-list>
|
||||
${this.group.members.length
|
||||
? this.group.members.map(
|
||||
(member) =>
|
||||
html`<a
|
||||
href="/config/devices/device/${member.device
|
||||
.device_reg_id}"
|
||||
>
|
||||
<ha-list-item
|
||||
>${member.device.user_given_name ||
|
||||
member.device.name}</ha-list-item
|
||||
>
|
||||
</a>`
|
||||
)
|
||||
: html`
|
||||
<ha-list-item> This group has no members </ha-list-item>
|
||||
`}
|
||||
</ha-list>
|
||||
</ha-card>
|
||||
${this.group.members.length
|
||||
? html`
|
||||
<div class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.remove_members"
|
||||
)}
|
||||
</div>
|
||||
|
||||
<zha-device-endpoint-data-table
|
||||
id="removeMembers"
|
||||
.hass=${this.hass}
|
||||
.deviceEndpoints=${this.group.members}
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleRemoveSelectionChanged}
|
||||
<div class="card-header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.group_info")}
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
<div>
|
||||
<span class="summary-label"
|
||||
>${this.hass.localize("ui.common.name")}</span
|
||||
>
|
||||
</zha-device-endpoint-data-table>
|
||||
<span class="summary-value">${this.group.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="summary-label"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.group_id"
|
||||
)}</span
|
||||
>
|
||||
<span class="summary-value"
|
||||
>${formatAsPaddedHex(this.group.group_id)}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="summary-label"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.members"
|
||||
)}</span
|
||||
>
|
||||
<span class="summary-value">${this.group.members.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
variant="danger"
|
||||
.disabled=${!this._selectedDevicesToRemove.length ||
|
||||
this._processingRemove}
|
||||
@click=${this._removeMembersFromGroup}
|
||||
class="button"
|
||||
.loading=${this._processingRemove}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.remove_members"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="header">
|
||||
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
|
||||
<div class="members-section">
|
||||
<h2>${this.hass.localize("ui.panel.config.zha.groups.members")}</h2>
|
||||
${this.group.members.length
|
||||
? html`
|
||||
<zha-device-endpoint-list
|
||||
id="removeMembers"
|
||||
scrollable
|
||||
show-device-link
|
||||
selectable
|
||||
.deviceEndpoints=${this.group.members}
|
||||
.narrow=${this.narrow}
|
||||
.emptyText=${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.no_members"
|
||||
)}
|
||||
@selection-changed=${this._handleRemoveSelectionChanged}
|
||||
></zha-device-endpoint-list>
|
||||
`
|
||||
: html`
|
||||
<ha-card class="empty-card">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.no_members"
|
||||
)}
|
||||
</ha-card>
|
||||
`}
|
||||
<div class="buttons">
|
||||
${this.group.members.length
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
variant="danger"
|
||||
.disabled=${!this._selectedDevicesToRemove.length ||
|
||||
this._processingRemove}
|
||||
@click=${this._removeMembersFromGroup}
|
||||
.loading=${this._processingRemove}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.groups.remove_members"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-button @click=${this._showAddMembersDialog}>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.zha.groups.add_members")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<zha-device-endpoint-data-table
|
||||
id="addMembers"
|
||||
.hass=${this.hass}
|
||||
.deviceEndpoints=${this._filteredDeviceEndpoints}
|
||||
.narrow=${this.narrow}
|
||||
selectable
|
||||
@selection-changed=${this._handleAddSelectionChanged}
|
||||
>
|
||||
</zha-device-endpoint-data-table>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
.disabled=${!this._selectedDevicesToAdd.length ||
|
||||
this._processingAdd}
|
||||
@click=${this._addMembersToGroup}
|
||||
class="button"
|
||||
.loading=${this._processingAdd}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zha.groups.add_members"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-config-section>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showAddMembersDialog(): void {
|
||||
showZHAAddGroupMembersDialog(this, {
|
||||
groupId: this.groupId,
|
||||
groupName: this.group!.name,
|
||||
devicesAddedCallback: (group) => {
|
||||
this.group = group;
|
||||
this._selectedDevicesToRemove = [];
|
||||
this._zhaRemoveMembersList?.clearSelection();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
if (this.groupId !== null && this.groupId !== undefined) {
|
||||
this.group = await fetchGroup(this.hass!, this.groupId);
|
||||
this.group = await fetchGroup(this.hass, this.groupId);
|
||||
}
|
||||
this.deviceEndpoints = await fetchGroupableDevices(this.hass!);
|
||||
// filter the groupable devices so we only show devices that aren't already in the group
|
||||
this._filterDevices();
|
||||
}
|
||||
|
||||
private _filterDevices() {
|
||||
// filter the groupable devices so we only show devices that aren't already in the group
|
||||
this._filteredDeviceEndpoints = this.deviceEndpoints.filter(
|
||||
(deviceEndpoint) =>
|
||||
!this.group!.members.some(
|
||||
(member) =>
|
||||
member.device.ieee === deviceEndpoint.device.ieee &&
|
||||
member.endpoint_id === deviceEndpoint.endpoint_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleAddSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToAdd = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleRemoveSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
ev: HASSDomEvent<DeviceEndpointSelectionChangedEvent>
|
||||
): void {
|
||||
this._selectedDevicesToRemove = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _addMembersToGroup(): Promise<void> {
|
||||
this._processingAdd = true;
|
||||
const members = this._selectedDevicesToAdd.map((member) => {
|
||||
const memberParts = member.split("_");
|
||||
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
|
||||
});
|
||||
this.group = await addMembersToGroup(this.hass, this.groupId, members);
|
||||
this._filterDevices();
|
||||
this._selectedDevicesToAdd = [];
|
||||
this._zhaAddMembersDataTable.clearSelection();
|
||||
this._processingAdd = false;
|
||||
}
|
||||
|
||||
private async _removeMembersFromGroup(): Promise<void> {
|
||||
this._processingRemove = true;
|
||||
const members = this._selectedDevicesToRemove.map((member) => {
|
||||
@@ -268,9 +208,8 @@ export class ZHAGroupPage extends LitElement {
|
||||
return { ieee: memberParts[0], endpoint_id: memberParts[1] };
|
||||
});
|
||||
this.group = await removeMembersFromGroup(this.hass, this.groupId, members);
|
||||
this._filterDevices();
|
||||
this._selectedDevicesToRemove = [];
|
||||
this._zhaRemoveMembersDataTable.clearSelection();
|
||||
this._zhaRemoveMembersList.clearSelection();
|
||||
this._processingRemove = false;
|
||||
}
|
||||
|
||||
@@ -285,30 +224,78 @@ export class ZHAGroupPage extends LitElement {
|
||||
hass-subpage {
|
||||
--app-header-text-color: var(--sidebar-icon-color);
|
||||
}
|
||||
.header {
|
||||
font-family: var(--ha-font-family-body);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
font-size: var(--ha-font-size-4xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4) var(--ha-space-4)
|
||||
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
.button {
|
||||
float: right;
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--ha-space-4);
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
.summary-label,
|
||||
.summary-value {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
margin-top: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-l);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.members-section {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
align-items: flex-end;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
justify-content: flex-end;
|
||||
padding: var(--ha-space-4) 0 0;
|
||||
}
|
||||
.buttons .warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
|
||||
.empty-card {
|
||||
padding: var(--ha-space-6);
|
||||
color: var(--secondary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -410,9 +410,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
if (!this._searchParms.has("label")) {
|
||||
if (
|
||||
!this._searchParms.has("area") &&
|
||||
!this._searchParms.has("device") &&
|
||||
!this._searchParms.has("label")
|
||||
) {
|
||||
this._filters = this._storageFilters;
|
||||
}
|
||||
this._filterArea();
|
||||
this._filterDevice();
|
||||
this._filterLabel();
|
||||
}
|
||||
}
|
||||
@@ -454,7 +460,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -785,6 +793,38 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterArea() {
|
||||
const area = this._searchParms.get("area");
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: [area] },
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterDevice() {
|
||||
const device = this._searchParms.get("device");
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-devices": {
|
||||
value: [device],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
|
||||
@@ -431,7 +431,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -783,10 +785,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
const hasUrlFilter =
|
||||
this._searchParms.has("blueprint") || this._searchParms.has("label");
|
||||
this._searchParms.has("area") ||
|
||||
this._searchParms.has("blueprint") ||
|
||||
this._searchParms.has("device") ||
|
||||
this._searchParms.has("label");
|
||||
if (!hasUrlFilter) {
|
||||
this._filters = this._storageFilters;
|
||||
}
|
||||
if (this._searchParms.has("area")) {
|
||||
this._filterArea();
|
||||
}
|
||||
if (this._searchParms.has("device")) {
|
||||
this._filterDevice();
|
||||
}
|
||||
if (this._searchParms.has("blueprint")) {
|
||||
this._filterBlueprint();
|
||||
}
|
||||
@@ -803,6 +814,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterArea() {
|
||||
const area = this._searchParms.get("area");
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: [area] },
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterDevice() {
|
||||
const device = this._searchParms.get("device");
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-devices": {
|
||||
value: [device],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import "../../layouts/hass-error-screen";
|
||||
import "../../layouts/hass-subpage";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { IFRAME_SANDBOX } from "../../util/iframe";
|
||||
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../util/iframe";
|
||||
|
||||
@customElement("ha-panel-iframe")
|
||||
class HaPanelIframe extends LitElement {
|
||||
@@ -41,7 +41,7 @@ class HaPanelIframe extends LitElement {
|
||||
this.panel.title === null ? undefined : this.panel.title
|
||||
)}
|
||||
src=${this.panel.config.url}
|
||||
.sandbox=${IFRAME_SANDBOX}
|
||||
.sandbox=${IFRAME_SANDBOX_SAME_ORIGIN}
|
||||
allow="fullscreen"
|
||||
></iframe>
|
||||
</hass-subpage>
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiPlayPause,
|
||||
mdiPower,
|
||||
mdiPowerStandby,
|
||||
mdiPowerOff,
|
||||
mdiPowerOn,
|
||||
mdiRepeat,
|
||||
@@ -225,7 +225,7 @@ class HuiMediaPlayerPlaybackCardFeature
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF)
|
||||
) {
|
||||
buttons.push({
|
||||
icon: assumedState ? mdiPowerOff : mdiPower,
|
||||
icon: assumedState ? mdiPowerOff : mdiPowerStandby,
|
||||
action: "turn_off",
|
||||
});
|
||||
}
|
||||
@@ -237,7 +237,7 @@ class HuiMediaPlayerPlaybackCardFeature
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
|
||||
) {
|
||||
buttons.push({
|
||||
icon: assumedState ? mdiPowerOn : mdiPower,
|
||||
icon: assumedState ? mdiPowerOn : mdiPowerStandby,
|
||||
action: "turn_on",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { IframeCardConfig } from "./types";
|
||||
import { IFRAME_SANDBOX } from "../../../util/iframe";
|
||||
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../../util/iframe";
|
||||
|
||||
@customElement("hui-iframe-card")
|
||||
export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
@@ -95,7 +95,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
const sandbox_params = this._config.disable_sandbox
|
||||
? undefined
|
||||
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
|
||||
: `${sandbox_user_params} ${IFRAME_SANDBOX_SAME_ORIGIN}`;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
|
||||
@@ -753,9 +753,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
ha-icon-button[action="media_play"],
|
||||
ha-icon-button[action="media_play_pause"],
|
||||
ha-icon-button[action="media_pause"],
|
||||
ha-icon-button[action="media_stop"],
|
||||
ha-icon-button[action="turn_on"],
|
||||
ha-icon-button[action="turn_off"] {
|
||||
ha-icon-button[action="media_stop"] {
|
||||
--ha-icon-button-size: 56px;
|
||||
--mdc-icon-size: 40px;
|
||||
}
|
||||
@@ -844,8 +842,10 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
|
||||
.narrow ha-icon-button[action="media_play"],
|
||||
.narrow ha-icon-button[action="media_play_pause"],
|
||||
.narrow ha-icon-button[action="media_pause"],
|
||||
.narrow ha-icon-button[action="turn_on"] {
|
||||
.narrow
|
||||
ha-icon-button[action="media_pause"]
|
||||
.narrow
|
||||
ha-icon-button[action="media_stop"] {
|
||||
--ha-icon-button-size: 50px;
|
||||
--mdc-icon-size: 36px;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
} from "../../../common/number/format_number";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-state-icon";
|
||||
@@ -226,7 +229,14 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||
? ""
|
||||
: this._value === null
|
||||
? "?"
|
||||
: formatNumber(this._value, this.hass.locale)}</span
|
||||
: formatNumber(
|
||||
this._value,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._config.entity]
|
||||
)
|
||||
)}</span
|
||||
>
|
||||
<span class="measurement"
|
||||
>${this._config.unit ||
|
||||
|
||||
@@ -229,9 +229,6 @@ export class HuiEntityEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-entity-picker {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -253,6 +250,11 @@ export class HuiEntityEditor extends LitElement {
|
||||
ha-md-list {
|
||||
gap: 8px;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-md-list:has(> *) {
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-md-list-item {
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronDown,
|
||||
mdiChevronRight,
|
||||
mdiChevronLeft,
|
||||
mdiMagnify,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
@@ -16,6 +17,7 @@ import { computeEntityName } from "../../../../common/entity/compute_entity_name
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import { mainWindow } from "../../../../common/dom/get_main_window";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
@@ -294,7 +296,11 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
private _renderChevron(expanded: boolean): TemplateResult {
|
||||
return html`<ha-svg-icon
|
||||
class="chevron"
|
||||
.path=${expanded ? mdiChevronDown : mdiChevronRight}
|
||||
.path=${expanded
|
||||
? mdiChevronDown
|
||||
: mainWindow.document.dir === "rtl"
|
||||
? mdiChevronLeft
|
||||
: mdiChevronRight}
|
||||
></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,10 @@ export class HuiViewEditor extends LitElement {
|
||||
const data = {
|
||||
...this._config,
|
||||
type: this._type,
|
||||
theme:
|
||||
this._config.theme?.toLowerCase() === "backend-selected"
|
||||
? undefined
|
||||
: this._config.theme,
|
||||
};
|
||||
|
||||
if (data.max_columns === undefined && this._type === SECTIONS_VIEW_LAYOUT) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
mdiPause,
|
||||
mdiPlay,
|
||||
mdiPlayPause,
|
||||
mdiPower,
|
||||
mdiPowerStandby,
|
||||
mdiPowerOff,
|
||||
mdiPowerOn,
|
||||
mdiSkipNext,
|
||||
@@ -198,7 +198,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
entityState !== UNAVAILABLE
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${assumedState ? mdiPowerOn : mdiPower}
|
||||
.path=${assumedState ? mdiPowerOn : mdiPowerStandby}
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_on")}
|
||||
@click=${this._turnOn}
|
||||
></ha-icon-button>
|
||||
@@ -216,7 +216,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
(stateActive(stateObj) || assumedState)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${assumedState ? mdiPowerOff : mdiPower}
|
||||
.path=${assumedState ? mdiPowerOff : mdiPowerStandby}
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_off")}
|
||||
@click=${this._turnOff}
|
||||
></ha-icon-button>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { normalizeLuminance } from "../../common/color/palette";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import "../../components/ha-theme-picker";
|
||||
import "../../components/input/ha-input";
|
||||
import "../../components/radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "../../components/radio/ha-radio-group";
|
||||
@@ -20,11 +19,14 @@ import {
|
||||
DefaultAccentColor,
|
||||
DefaultPrimaryColor,
|
||||
} from "../../resources/theme/color/color.globals";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
ThemeSettings,
|
||||
ValueChangedEvent,
|
||||
} from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { clearSelectedThemeState } from "../../util/ha-pref-storage";
|
||||
|
||||
const USE_DEFAULT_THEME = "__USE_DEFAULT_THEME__";
|
||||
const HOME_ASSISTANT_THEME = "default";
|
||||
|
||||
@customElement("ha-pick-theme-row")
|
||||
@@ -33,8 +35,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() _themeNames: string[] = [];
|
||||
|
||||
@state() private _userTheme?: ThemeSettings | null;
|
||||
|
||||
@state() private _migrating = false;
|
||||
@@ -88,24 +88,17 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.panel.profile.themes.link_promo")}
|
||||
</a>
|
||||
</span>
|
||||
<ha-select
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
|
||||
.noThemeLabel=${this.hass.localize(
|
||||
"ui.panel.profile.themes.use_default"
|
||||
)}
|
||||
.value=${this.hass.selectedTheme?.theme || undefined}
|
||||
.disabled=${!hasThemes}
|
||||
.value=${this.hass.selectedTheme?.theme || USE_DEFAULT_THEME}
|
||||
@selected=${this._handleThemeSelection}
|
||||
.options=${[
|
||||
{
|
||||
value: USE_DEFAULT_THEME,
|
||||
label: this.hass.localize("ui.panel.profile.themes.use_default"),
|
||||
},
|
||||
{ value: HOME_ASSISTANT_THEME, label: "Home Assistant" },
|
||||
...this._themeNames.map((theme) => ({
|
||||
value: theme,
|
||||
label: theme,
|
||||
})),
|
||||
]}
|
||||
>
|
||||
</ha-select>
|
||||
include-default
|
||||
@value-changed=${this._handleThemeSelection}
|
||||
></ha-theme-picker>
|
||||
</ha-settings-row>
|
||||
${curTheme === HOME_ASSISTANT_THEME ||
|
||||
(curThemeIsUseDefault &&
|
||||
@@ -194,17 +187,6 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues<this>) {
|
||||
const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
|
||||
const themesChanged =
|
||||
changedProperties.has("hass") &&
|
||||
(!oldHass || oldHass.themes.themes !== this.hass.themes.themes);
|
||||
|
||||
if (themesChanged) {
|
||||
this._themeNames = Object.keys(this.hass.themes.themes).sort();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleColorChange(ev: CustomEvent) {
|
||||
const target = ev.target as any;
|
||||
|
||||
@@ -245,13 +227,14 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "settheme", { dark });
|
||||
}
|
||||
|
||||
private _handleThemeSelection(ev: HaSelectSelectEvent) {
|
||||
private _handleThemeSelection(
|
||||
ev: ValueChangedEvent<string | undefined>
|
||||
): void {
|
||||
ev.stopPropagation();
|
||||
const theme = ev.detail.value;
|
||||
if (theme === this.hass.selectedTheme?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === USE_DEFAULT_THEME) {
|
||||
if (theme === undefined) {
|
||||
// undefined = "use default"
|
||||
if (this.hass.selectedTheme?.theme) {
|
||||
fireEvent(this, "settheme", {
|
||||
theme: "",
|
||||
@@ -261,6 +244,11 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === this.hass.selectedTheme?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "settheme", {
|
||||
theme,
|
||||
primaryColor: undefined,
|
||||
@@ -320,7 +308,7 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
ha-select {
|
||||
ha-theme-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -313,8 +313,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
});
|
||||
clearInterval(this.__backendPingInterval);
|
||||
|
||||
// Fetch the brands access token on initial connect and schedule refresh
|
||||
fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
this._refreshBrandsAccessToken();
|
||||
|
||||
this.__backendPingInterval = setInterval(() => {
|
||||
if (this.hass?.connected) {
|
||||
@@ -340,8 +339,7 @@ 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!);
|
||||
this._refreshBrandsAccessToken();
|
||||
|
||||
// on reconnect always fetch config as we might miss an update while we were disconnected
|
||||
// @ts-ignore
|
||||
@@ -362,4 +360,15 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
clearInterval(this.__backendPingInterval);
|
||||
clearBrandsTokenRefresh();
|
||||
}
|
||||
|
||||
private async _refreshBrandsAccessToken() {
|
||||
// The brands WS handler may not be registered yet after a server restart;
|
||||
// fetchAndScheduleBrandsAccessToken retries internally. If the token
|
||||
// changed, re-render so any brand <img> elements that rendered against a
|
||||
// different (or missing) token recompute their src and re-fetch.
|
||||
const changed = await fetchAndScheduleBrandsAccessToken(this.hass!);
|
||||
if (changed) {
|
||||
this._updateHass({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1969,6 +1969,7 @@
|
||||
"entity_disabled": "This entity is disabled.",
|
||||
"enable_entity": "Enable",
|
||||
"open_device_settings": "Open device settings",
|
||||
"device_name_tip": "Consider renaming the device instead to update all its entities at once. {link}",
|
||||
"switch_as_x_confirm": "This switch will be hidden and a new {domain} will be added. Your existing configurations using the switch will continue to work.",
|
||||
"switch_as_x_remove_confirm": "This {domain} will be removed and the original switch will be visible again. Your existing configurations using the {domain} will no longer work!",
|
||||
"switch_as_x_change_confirm": "This {domain_1} will be removed and will be replaced by a new {domain_2}. Your existing configurations using the {domain_1} will no longer work!",
|
||||
@@ -2717,6 +2718,14 @@
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"quick_links": {
|
||||
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
|
||||
"helpers": "{count} {count, plural,\n one {helper}\n other {helpers}\n}",
|
||||
"automations": "{count} {count, plural,\n one {automation}\n other {automations}\n}",
|
||||
"scenes": "{count} {count, plural,\n one {scene}\n other {scenes}\n}",
|
||||
"scripts": "{count} {count, plural,\n one {script}\n other {scripts}\n}"
|
||||
},
|
||||
"editor": {
|
||||
"confirm_unsaved": "You have unsaved changes. Are you sure you want to leave?"
|
||||
},
|
||||
@@ -2735,6 +2744,11 @@
|
||||
"updates": {
|
||||
"caption": "Updates",
|
||||
"description": "Manage updates of Home Assistant, apps, and devices",
|
||||
"group_system": "Home Assistant",
|
||||
"group_integrations": "Integrations",
|
||||
"group_apps": "Apps",
|
||||
"update_all": "Update all",
|
||||
"update_all_failed": "Failed to start updates",
|
||||
"no_updates": "No updates available",
|
||||
"no_update_entities": {
|
||||
"title": "Unable to check for updates",
|
||||
@@ -3127,6 +3141,14 @@
|
||||
"caption": "Areas",
|
||||
"description": "Group devices and entities into areas",
|
||||
"edit_settings": "Area settings",
|
||||
"quick_links": {
|
||||
"devices": "[%key:ui::panel::config::common::quick_links::devices%]",
|
||||
"entities": "[%key:ui::panel::config::common::quick_links::entities%]",
|
||||
"helpers": "[%key:ui::panel::config::common::quick_links::helpers%]",
|
||||
"automations": "[%key:ui::panel::config::common::quick_links::automations%]",
|
||||
"scenes": "[%key:ui::panel::config::common::quick_links::scenes%]",
|
||||
"scripts": "[%key:ui::panel::config::common::quick_links::scripts%]"
|
||||
},
|
||||
"add_picture": "Add a picture",
|
||||
"assigned_to_area": "Assigned to this area",
|
||||
"targeting_area": "Targeting this area",
|
||||
@@ -5164,10 +5186,9 @@
|
||||
},
|
||||
"id": "Trigger ID",
|
||||
"optional": "Optional",
|
||||
"add_id": "Add trigger ID",
|
||||
"edit_id": "Edit trigger ID",
|
||||
"id_description": "Use trigger IDs in a Triggered by condition to run different actions depending on which trigger started the automation.",
|
||||
"edit_id": "Edit ID",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"duplicate_id_warning": "This trigger ID is used multiple times in this automation. Trigger IDs should be unique.",
|
||||
"re_order": "Re-order",
|
||||
"rename": "Rename",
|
||||
"cut": "Cut",
|
||||
@@ -5593,6 +5614,8 @@
|
||||
"trigger": {
|
||||
"label": "Triggered by",
|
||||
"no_triggers": "There are no triggers with ID's set in this automation. Edit a trigger and give it a Trigger ID name.",
|
||||
"duplicated_info": "This ID is used by multiple triggers. Trigger IDs should be unique.",
|
||||
"unavailable_info": "No trigger has the ID {id}. Set this ID on a trigger to use it.",
|
||||
"id": "Trigger",
|
||||
"description": {
|
||||
"picker": "Tests if the automation has been triggered by a specific trigger.",
|
||||
@@ -6469,6 +6492,13 @@
|
||||
"device_info": "{type} info",
|
||||
"edit_settings": "Edit settings",
|
||||
"restore_entity_ids": "Recreate entity IDs",
|
||||
"quick_links": {
|
||||
"entities": "[%key:ui::panel::config::common::quick_links::entities%]",
|
||||
"helpers": "[%key:ui::panel::config::common::quick_links::helpers%]",
|
||||
"automations": "[%key:ui::panel::config::common::quick_links::automations%]",
|
||||
"scenes": "[%key:ui::panel::config::common::quick_links::scenes%]",
|
||||
"scripts": "[%key:ui::panel::config::common::quick_links::scripts%]"
|
||||
},
|
||||
"unnamed_device": "Unnamed {type}",
|
||||
"unknown_error": "Unknown error",
|
||||
"name": "Name",
|
||||
@@ -7108,6 +7138,7 @@
|
||||
"scanning_mode_passive": "passive",
|
||||
"scanning_mode_active_label": "Active scanning",
|
||||
"scanning_mode_passive_label": "Passive scanning",
|
||||
"scanning_mode_auto_with_current": "Auto ({current})",
|
||||
"scanning_mode_none_label": "No scanning",
|
||||
"scanner_mode_mismatch": "{name} requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
|
||||
"scanner_mode_mismatch_remote": "For proxies: reboot the device",
|
||||
@@ -7125,6 +7156,7 @@
|
||||
"manufacturer_data": "Manufacturer data",
|
||||
"service_data": "Service data",
|
||||
"service_uuids": "Service UUIDs",
|
||||
"raw_advertisement": "Raw advertisement",
|
||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||
"area": "Area",
|
||||
"scanners": "Scanners",
|
||||
@@ -7326,8 +7358,15 @@
|
||||
"group_details": "Here are all the details for the selected Zigbee group.",
|
||||
"group_not_found": "Group not found!",
|
||||
"add_members": "Add devices",
|
||||
"remove_members": "Remove device",
|
||||
"remove_members": "Remove devices",
|
||||
"removing_members": "Removing devices",
|
||||
"no_members": "This group has no devices",
|
||||
"no_devices_found": "No devices found",
|
||||
"no_devices_to_add": "No devices to add",
|
||||
"no_entities": "No entities",
|
||||
"entity_count": "{count} entity",
|
||||
"entity_count_plural": "{count} entities",
|
||||
"open_device": "Open device",
|
||||
"create_group_details": "Enter the required details to create a new Zigbee group",
|
||||
"group_name_placeholder": "Group name",
|
||||
"group_id_placeholder": "Group ID (optional)",
|
||||
|
||||
+28
-7
@@ -1,3 +1,4 @@
|
||||
import { waitForMs } from "../common/util/wait";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface BrandsOptions {
|
||||
@@ -20,15 +21,35 @@ let _brandsRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
||||
// Re-fetch every 30 minutes to always have a valid token.
|
||||
const TOKEN_REFRESH_MS = 30 * 60 * 1000;
|
||||
|
||||
export const fetchAndScheduleBrandsAccessToken = (
|
||||
// Delays before each attempt. The first attempt fires immediately; subsequent
|
||||
// ones back off to ride through the window after a Home Assistant restart
|
||||
// where the WebSocket server accepts connections but the brands integration
|
||||
// hasn't registered its WS handler yet. On older backends without the command,
|
||||
// every attempt fails and we give up.
|
||||
const FETCH_DELAYS_MS = [0, 500, 1000, 2000, 5000, 10000, 15000];
|
||||
|
||||
// Returns true if the cached token changed as a result of this call, so
|
||||
// callers can decide whether they need to trigger a re-render.
|
||||
export const fetchAndScheduleBrandsAccessToken = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<void> =>
|
||||
fetchBrandsAccessToken(hass).then(
|
||||
() => scheduleBrandsTokenRefresh(hass),
|
||||
() => {
|
||||
// Ignore failures; older backends may not support this command
|
||||
): Promise<boolean> => {
|
||||
const previousToken = _brandsAccessToken;
|
||||
/* eslint-disable no-await-in-loop -- retries are intentionally sequential */
|
||||
for (const delay of FETCH_DELAYS_MS) {
|
||||
if (delay) {
|
||||
await waitForMs(delay);
|
||||
}
|
||||
);
|
||||
try {
|
||||
await fetchBrandsAccessToken(hass);
|
||||
scheduleBrandsTokenRefresh(hass);
|
||||
return _brandsAccessToken !== previousToken;
|
||||
} catch {
|
||||
// try next delay
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
return false;
|
||||
};
|
||||
|
||||
export const fetchBrandsAccessToken = async (
|
||||
hass: HomeAssistant
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user