mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-14 11:17:26 +00:00
Compare commits
76 Commits
bluetooth_
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7572257821 | ||
|
|
4703cf802f | ||
|
|
55c2315329 | ||
|
|
7d7e95ac55 | ||
|
|
6d7694caff | ||
|
|
d7b6243698 | ||
|
|
73feef9e92 | ||
|
|
453a546574 | ||
|
|
52c0e6f1f5 | ||
|
|
444f8d87b3 | ||
|
|
57a586c3a7 | ||
|
|
1975265e6b | ||
|
|
66e6cb8dbc | ||
|
|
9ce9d254f8 | ||
|
|
1beca4bfa6 | ||
|
|
82ab29cfc5 | ||
|
|
3579c66f71 | ||
|
|
c042a8e310 | ||
|
|
8d2794a4ee | ||
|
|
50be1d9345 | ||
|
|
c551bf03b6 | ||
|
|
cd062293fc | ||
|
|
e89ea47d3a | ||
|
|
2cd209a6a4 | ||
|
|
9bbc761736 | ||
|
|
9097faa04b | ||
|
|
fcf844cf1a | ||
|
|
8808c31e98 | ||
|
|
e0a9f5a08a | ||
|
|
56d71c8e54 | ||
|
|
125ab4c671 | ||
|
|
8014216c45 | ||
|
|
55ba331489 | ||
|
|
ad2ff672b0 | ||
|
|
00907ecd17 | ||
|
|
07d8219136 | ||
|
|
f37241c84c | ||
|
|
65d046132d | ||
|
|
122cf40092 | ||
|
|
28ed5c86c7 | ||
|
|
1f99c3d895 | ||
|
|
f2293713de | ||
|
|
b3f202400c | ||
|
|
010d87bd0d | ||
|
|
b403b8f09e | ||
|
|
b9a3dc795b | ||
|
|
35dbfdebcf | ||
|
|
c5e5fb3ace | ||
|
|
e649472b20 | ||
|
|
3cbb24a4c5 | ||
|
|
f92608a9d3 | ||
|
|
6591cdc5c1 | ||
|
|
0ae1ac367d | ||
|
|
6d3a1b93e1 | ||
|
|
6d7b22a21c | ||
|
|
784ee22623 | ||
|
|
c03654ef8e | ||
|
|
826cb3117d | ||
|
|
f77fa26ffe | ||
|
|
35e30f9184 | ||
|
|
7dd3ade678 | ||
|
|
6d1e15d11a | ||
|
|
f5b33922ff | ||
|
|
ceb7baf851 | ||
|
|
d195fd3244 | ||
|
|
231cd632d6 | ||
|
|
82d72ea39c | ||
|
|
022bebb14f | ||
|
|
0981ae1b4a | ||
|
|
9608824a28 | ||
|
|
33d215533e | ||
|
|
5c503ecac0 | ||
|
|
d114693fed | ||
|
|
7a8cb80413 | ||
|
|
f5cd234c4b | ||
|
|
49bed5e6a6 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
/node_modules/
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
|
||||
@@ -213,7 +213,9 @@ const createRspackConfig = ({
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/directives/keyed$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
|
||||
40
package.json
40
package.json
@@ -34,18 +34,18 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.3",
|
||||
"@codemirror/view": "6.39.8",
|
||||
"@codemirror/view": "6.39.9",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.1.1",
|
||||
"@formatjs/intl-displaynames": "7.1.1",
|
||||
"@formatjs/intl-durationformat": "0.9.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.1",
|
||||
"@formatjs/intl-listformat": "8.1.1",
|
||||
"@formatjs/intl-locale": "5.1.1",
|
||||
"@formatjs/intl-numberformat": "9.1.1",
|
||||
"@formatjs/intl-pluralrules": "6.1.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.1",
|
||||
"@formatjs/intl-datetimeformat": "7.1.2",
|
||||
"@formatjs/intl-displaynames": "7.1.2",
|
||||
"@formatjs/intl-durationformat": "0.9.2",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.2",
|
||||
"@formatjs/intl-listformat": "8.1.2",
|
||||
"@formatjs/intl-locale": "5.1.2",
|
||||
"@formatjs/intl-numberformat": "9.1.2",
|
||||
"@formatjs/intl-pluralrules": "6.1.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.2",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -112,13 +112,13 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.0.8",
|
||||
"intl-messageformat": "11.0.9",
|
||||
"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",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -150,13 +150,13 @@
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.8",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.4.0",
|
||||
"@rspack/core": "1.7.0",
|
||||
"@rspack/core": "1.7.1",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -215,8 +215,8 @@
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.51.0",
|
||||
"vite-tsconfig-paths": "6.0.3",
|
||||
"typescript-eslint": "8.52.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.16",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -224,8 +224,8 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
@@ -236,6 +236,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.12.0"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,11 @@ export class HaAuthFormString extends HaFormString {
|
||||
}
|
||||
</style>
|
||||
<ha-auth-textfield
|
||||
.type=${
|
||||
!this.isPassword
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
@@ -55,18 +53,17 @@ export class HaAuthFormString extends HaFormString {
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
?autofocus=${this.schema.autofocus}
|
||||
.suffix=${
|
||||
this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix
|
||||
}
|
||||
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.panel.page-authorize.form.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const generateColorPalette = (
|
||||
}
|
||||
|
||||
return steps.map((step) => {
|
||||
const name = `color-${label}-${step}`;
|
||||
const name = `ha-color-${label}-${step}`;
|
||||
|
||||
// Base color at 50%
|
||||
if (step === 50) {
|
||||
|
||||
@@ -93,8 +93,8 @@ export const calcDateRange = (
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
|
||||
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 12),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 0),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
|
||||
53
src/common/lit/keyed-es5.ts
Normal file
53
src/common/lit/keyed-es5.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ES5-compatible implementation of the keyed directive.
|
||||
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
|
||||
*
|
||||
* This implementation avoids parameter destructuring in the update() method,
|
||||
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
|
||||
*
|
||||
* Used only for ES5 builds (legacy browsers). Modern builds use the original
|
||||
* lit-html keyed directive.
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/28732
|
||||
*/
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { directive, Directive } from "lit-html/directive.js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { setCommittedValue } from "lit-html/directive-helpers.js";
|
||||
// eslint-disable-next-line lit/no-legacy-imports
|
||||
import { nothing } from "lit-html";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import type { Part } from "lit-html/directive.js";
|
||||
|
||||
class KeyedES5 extends Directive {
|
||||
private _key: unknown = nothing;
|
||||
|
||||
render(k: unknown, v: unknown) {
|
||||
this._key = k;
|
||||
return v;
|
||||
}
|
||||
|
||||
update(part: unknown, args: [unknown, unknown]) {
|
||||
const k = args[0];
|
||||
const v = args[1];
|
||||
if (k !== this._key) {
|
||||
// Clear the part before returning a value. The one-arg form of
|
||||
// setCommittedValue sets the value to a sentinel which forces a
|
||||
// commit the next render.
|
||||
setCommittedValue(part as Part);
|
||||
this._key = k;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a renderable value with a unique key. When the key changes, the
|
||||
* previous DOM is removed and disposed before rendering the next value, even
|
||||
* if the value - such as a template - is the same.
|
||||
*
|
||||
* This is useful for forcing re-renders of stateful components, or working
|
||||
* with code that expects new data to generate new HTML elements, such as some
|
||||
* animation techniques.
|
||||
*/
|
||||
export const keyed = directive(KeyedES5);
|
||||
@@ -1,6 +1,16 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -184,7 +185,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
|
||||
@@ -1364,6 +1364,9 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: var(--float-start) 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell--numeric > span {
|
||||
transition: none;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-svg-icon {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -11,11 +12,12 @@ import {
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||
|
||||
export abstract class HaDeviceAutomationPicker<
|
||||
T extends DeviceAutomation,
|
||||
@@ -28,7 +30,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
@state() private _automations: T[] = [];
|
||||
@state() private _automations?: T[];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
// paper-listbox does not like changing things around.
|
||||
@@ -44,12 +46,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
@@ -75,7 +71,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
if (!this.value) {
|
||||
if (!this.value || !this._automations) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -88,7 +84,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
return UNKNOWN_AUTOMATION_KEY;
|
||||
return this.value.alias || this.value.type || "unknown";
|
||||
}
|
||||
|
||||
return `${this._automations[idx].device_id}_${idx}`;
|
||||
@@ -99,37 +95,21 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return nothing;
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-md-select>
|
||||
`;
|
||||
|
||||
return html`<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
)}
|
||||
hide-clear-icon
|
||||
>
|
||||
</ha-generic-picker>`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -140,6 +120,57 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
}
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(value: string, automations: T[] | undefined) => {
|
||||
if (!automations) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const automationListItems = automations.map((automation, idx) => {
|
||||
const primary = this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
);
|
||||
return {
|
||||
id: `${automation.device_id}_${idx}`,
|
||||
primary,
|
||||
};
|
||||
});
|
||||
|
||||
automationListItems.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.primary,
|
||||
b.primary,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
if (value === NO_AUTOMATION_KEY) {
|
||||
automationListItems.unshift({
|
||||
id: NO_AUTOMATION_KEY,
|
||||
primary: this.NO_AUTOMATION_TEXT,
|
||||
});
|
||||
}
|
||||
|
||||
return () => automationListItems;
|
||||
}
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const automation = this._automations?.find(
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
: value;
|
||||
|
||||
return html`<span slot="headline">${text}</span>`;
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
this._automations = this.deviceId
|
||||
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
|
||||
@@ -161,13 +192,14 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev) {
|
||||
const value = ev.target.value;
|
||||
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
return;
|
||||
}
|
||||
const [deviceId, idx] = value.split("_");
|
||||
const automation = this._automations[idx];
|
||||
const automation = this._automations![idx];
|
||||
if (automation.device_id !== deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
@@ -94,7 +95,30 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
_devices: HomeAssistant["devices"],
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getDevices(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value
|
||||
)
|
||||
);
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
@@ -110,7 +134,7 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getDevicesMemoized(
|
||||
this.hass,
|
||||
this.hass.devices,
|
||||
this._configEntryLookup,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
|
||||
@@ -275,6 +275,11 @@ export class HaEntityNamePicker extends LitElement {
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
|
||||
@@ -143,17 +143,19 @@ export class HaEntityToggle extends LitElement {
|
||||
// Optimistic update.
|
||||
this._isOn = turnOn;
|
||||
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
// If after 2 seconds we have not received a state update
|
||||
// reset the switch to it's original state.
|
||||
if (this.stateObj === currentState) {
|
||||
this._isOn = isOn(this.stateObj);
|
||||
}
|
||||
}, 2000);
|
||||
try {
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
} finally {
|
||||
setTimeout(async () => {
|
||||
// If after 2 seconds we have not received a state update
|
||||
// reset the switch to it's original state.
|
||||
if (this.stateObj === currentState) {
|
||||
this._isOn = isOn(this.stateObj);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -174,12 +174,14 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 var(--ha-space-3);
|
||||
}
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:host([sort-selected]) .row {
|
||||
outline: solid;
|
||||
|
||||
@@ -51,7 +51,10 @@ export class HaCard extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
:host
|
||||
::slotted(
|
||||
.card-content:not(:nth-child(1 of .card-content, .card-header))
|
||||
),
|
||||
slot:not(:first-child)::slotted(.card-content) {
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
|
||||
@@ -255,6 +255,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
...this._loadedCodeMirror.tabKeyBindings,
|
||||
saveKeyBinding,
|
||||
]),
|
||||
this._loadedCodeMirror.search({ top: true }),
|
||||
this._loadedCodeMirror.langCompartment.of(this._mode),
|
||||
this._loadedCodeMirror.haTheme,
|
||||
this._loadedCodeMirror.haSyntaxHighlighting,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-combo-box-textfield")
|
||||
export class HaComboBoxTextField extends HaTextField {
|
||||
@property({ type: Boolean, attribute: "force-blank-value" })
|
||||
public forceBlankValue = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
|
||||
if (this.forceBlankValue && this.value) {
|
||||
this.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-combo-box-textfield": HaComboBoxTextField;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -13,6 +15,8 @@ export interface HaDurationData {
|
||||
milliseconds?: number;
|
||||
}
|
||||
|
||||
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
|
||||
|
||||
@customElement("ha-duration-input")
|
||||
class HaDurationInput extends LitElement {
|
||||
@property({ attribute: false }) public data?: HaDurationData;
|
||||
@@ -29,41 +33,80 @@ class HaDurationInput extends LitElement {
|
||||
@property({ attribute: "enable-day", type: Boolean })
|
||||
public enableDay = false;
|
||||
|
||||
@property({ attribute: "allow-negative", type: Boolean })
|
||||
public allowNegative = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.clearable=${!this.required && this.data !== undefined}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
no-hours-limit
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
sec-label="ss"
|
||||
ms-label="ms"
|
||||
></ha-base-time-input>
|
||||
<div class="row">
|
||||
${this.allowNegative
|
||||
? html`
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${[
|
||||
{ label: "+", iconPath: mdiPlusThick, value: "+" },
|
||||
{ label: "-", iconPath: mdiMinusThick, value: "-" },
|
||||
]}
|
||||
.active=${this._negative ? "-" : "+"}
|
||||
@value-changed=${this._negativeChanged}
|
||||
></ha-button-toggle-group>
|
||||
`
|
||||
: nothing}
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.clearable=${!this.required && this.data !== undefined}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enable-second
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
no-hours-limit
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
sec-label="ss"
|
||||
ms-label="ms"
|
||||
></ha-base-time-input>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _negative() {
|
||||
return (
|
||||
this._toggleNegative ||
|
||||
(this.data?.days
|
||||
? this.data.days < 0
|
||||
: this.data?.hours
|
||||
? this.data.hours < 0
|
||||
: this.data?.minutes
|
||||
? this.data.minutes < 0
|
||||
: this.data?.seconds
|
||||
? this.data.seconds < 0
|
||||
: this.data?.milliseconds
|
||||
? this.data.milliseconds < 0
|
||||
: false)
|
||||
);
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days
|
||||
? Number(this.data.days)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.days))
|
||||
: Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -71,7 +114,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours
|
||||
? Number(this.data.hours)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.hours))
|
||||
: Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -79,7 +124,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _minutes() {
|
||||
return this.data?.minutes
|
||||
? Number(this.data.minutes)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.minutes))
|
||||
: Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -87,7 +134,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _seconds() {
|
||||
return this.data?.seconds
|
||||
? Number(this.data.seconds)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.seconds))
|
||||
: Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -95,7 +144,9 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _milliseconds() {
|
||||
return this.data?.milliseconds
|
||||
? Number(this.data.milliseconds)
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.milliseconds))
|
||||
: Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -113,6 +164,14 @@ class HaDurationInput extends LitElement {
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (this.allowNegative) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.enableMillisecond && !value.milliseconds) {
|
||||
// @ts-ignore
|
||||
delete value.milliseconds;
|
||||
@@ -135,12 +194,47 @@ class HaDurationInput extends LitElement {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
|
||||
if (this._negative) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = -Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private _negativeChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const negative = (ev.detail?.value || ev.target.value) === "-";
|
||||
this._toggleNegative = negative;
|
||||
const value = this.data;
|
||||
if (value) {
|
||||
FIELDS.forEach((t) => {
|
||||
if (value[t]) {
|
||||
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
|
||||
}
|
||||
});
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
198
src/components/ha-filter-voice-assistants.ts
Normal file
198
src/components/ha-filter-voice-assistants.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./voice-assistant-brand-icon";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// the list of selected voiceAssistantIds
|
||||
@property({ attribute: false }) public value: string[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _voiceAssistantOptions: string[] = [];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<ha-list
|
||||
@selected=${this._assistantsSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._voiceAssistantOptions,
|
||||
(voiceAssistantId) => voiceAssistantId,
|
||||
(voiceAssistantId) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${voiceAssistantId}
|
||||
.selected=${(this.value || []).includes(voiceAssistantId)}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
>
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._voiceAssistantOptions = Object.keys(voiceAssistants);
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("ha-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _assistantsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
) {
|
||||
if (!ev.detail.index) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const newvalue: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
newvalue.push(this._voiceAssistantOptions![index]);
|
||||
}
|
||||
this.value = newvalue;
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = [];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-voice-assistants": HaFilterVoiceAssistants;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,21 @@
|
||||
import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
import type { HaFormData, HaFormSchema } from "./types";
|
||||
|
||||
const setDefaultValue = (
|
||||
field: HaFormSchema,
|
||||
value: HaFormData | undefined
|
||||
) => {
|
||||
if ("selector" in field && "choose" in field.selector) {
|
||||
const firstChoice = Object.keys(field.selector.choose.choices)[0];
|
||||
if (firstChoice) {
|
||||
return {
|
||||
active_choice: firstChoice,
|
||||
[firstChoice]: value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[]
|
||||
@@ -10,9 +26,12 @@ export const computeInitialHaFormData = (
|
||||
field.description?.suggested_value !== undefined &&
|
||||
field.description?.suggested_value !== null
|
||||
) {
|
||||
data[field.name] = field.description.suggested_value;
|
||||
data[field.name] = setDefaultValue(
|
||||
field,
|
||||
field.description.suggested_value
|
||||
);
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
data[field.name] = setDefaultValue(field, field.default);
|
||||
} else if (field.type === "expandable") {
|
||||
const expandableData = computeInitialHaFormData(field.schema);
|
||||
if (field.required || Object.keys(expandableData).length) {
|
||||
@@ -108,6 +127,21 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = {};
|
||||
} else if ("state" in selector) {
|
||||
data[field.name] = selector.state?.multiple ? [] : "";
|
||||
} else if ("choose" in selector) {
|
||||
const firstChoice = Object.keys(selector.choose.choices)[0];
|
||||
if (!firstChoice) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
data[field.name] = {
|
||||
active_choice: firstChoice,
|
||||
[firstChoice]: computeInitialHaFormData([
|
||||
{
|
||||
name: firstChoice,
|
||||
selector: selector.choose.choices[firstChoice].selector,
|
||||
},
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type CSSResultGroup,
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -114,6 +121,8 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@state() private _openedNarrow = false;
|
||||
|
||||
@state() private _unknownValue = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
@@ -130,6 +139,25 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._setUnknownValue();
|
||||
return;
|
||||
}
|
||||
if (changedProperties.has("hass")) {
|
||||
this._throttleUnknownValue();
|
||||
}
|
||||
}
|
||||
|
||||
public setFieldValue(value: string) {
|
||||
if (this._comboBox) {
|
||||
this._comboBox.setFieldValue(value);
|
||||
return;
|
||||
}
|
||||
// Store initial value to set when opened
|
||||
this._initialFieldValue = value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
// Only show label if it's not a top label and there is a value.
|
||||
const label = this.useTopLabel && this.value ? undefined : this.label;
|
||||
@@ -157,11 +185,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue(
|
||||
this.allowCustomValue,
|
||||
this.value,
|
||||
this.getItems()
|
||||
)}
|
||||
.unknown=${this._unknownValue}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@@ -182,40 +206,42 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
</ha-picker-field>`}
|
||||
</slot>
|
||||
</div>
|
||||
${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
|
||||
? html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="picker"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox()}
|
||||
</wa-popover>
|
||||
`
|
||||
: this._pickerWrapperOpen || this._opened
|
||||
? html`<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox(true)}
|
||||
</ha-bottom-sheet>`
|
||||
: nothing}
|
||||
${this._pickerWrapperOpen || this._opened
|
||||
? this._openedNarrow
|
||||
? html`
|
||||
<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox(true)}
|
||||
</ha-bottom-sheet>
|
||||
`
|
||||
: html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
without-arrow
|
||||
distance="-4"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="picker"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label=${this.label || "Select option"}
|
||||
>
|
||||
${this._renderComboBox()}
|
||||
</wa-popover>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderHelper()}`;
|
||||
}
|
||||
@@ -248,26 +274,29 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _unknownValue = memoizeOne(
|
||||
(
|
||||
allowCustomValue: boolean,
|
||||
value?: string,
|
||||
items?: (PickerComboBoxItem | string)[]
|
||||
) => {
|
||||
if (
|
||||
allowCustomValue ||
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === "" ||
|
||||
!items
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.some(
|
||||
(item) => typeof item !== "string" && item.id === value
|
||||
);
|
||||
private _setUnknownValue = () => {
|
||||
const items = this.getItems();
|
||||
if (
|
||||
this.allowCustomValue ||
|
||||
this.value === undefined ||
|
||||
this.value === null ||
|
||||
this.value === "" ||
|
||||
!items
|
||||
) {
|
||||
this._unknownValue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._unknownValue = !items.some(
|
||||
(item) => typeof item !== "string" && item.id === this.value
|
||||
);
|
||||
};
|
||||
|
||||
private _throttleUnknownValue = throttle(
|
||||
this._setUnknownValue,
|
||||
1000,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
@@ -283,9 +312,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
</ha-input-helper-text>`;
|
||||
}
|
||||
|
||||
private _initialFieldValue?: string;
|
||||
|
||||
private _dialogOpened = () => {
|
||||
this._opened = true;
|
||||
requestAnimationFrame(() => {
|
||||
// Set initial field value if needed
|
||||
if (this._initialFieldValue) {
|
||||
this._comboBox?.setFieldValue(this._initialFieldValue);
|
||||
this._initialFieldValue = undefined;
|
||||
}
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
@@ -295,6 +331,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._comboBox?.focus();
|
||||
});
|
||||
};
|
||||
@@ -376,6 +413,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.container {
|
||||
position: relative;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
label[disabled] {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -25,11 +25,9 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS = "___NO_LABELS___";
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -108,52 +106,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
private _labelMap = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined
|
||||
): Map<string, LabelRegistryEntry> => {
|
||||
if (!labels) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(labels.map((label) => [label.label_id, label]));
|
||||
}
|
||||
);
|
||||
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
|
||||
(value) => {
|
||||
const label = this._labelMap(labels).get(value);
|
||||
|
||||
if (!label) {
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${label.icon
|
||||
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
|
||||
<span slot="headline">${label.name}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getItems = () => {
|
||||
if (!this._labels || this._labels.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_LABELS,
|
||||
primary: this.hass.localize("ui.components.label-picker.no_labels"),
|
||||
icon_path: mdiLabel,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
private _getItems = () =>
|
||||
this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
@@ -166,7 +122,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -219,8 +174,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.label-picker.label");
|
||||
|
||||
const valueRenderer = this._computeValueRenderer(this._labels);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.disabled=${this.disabled}
|
||||
@@ -237,7 +190,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -251,10 +203,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_LABELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -153,6 +153,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
public setFieldValue(value: string) {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
@@ -787,7 +793,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
.section-title,
|
||||
.title {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
|
||||
@@ -10,7 +10,7 @@ class HaSectionTitle extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
|
||||
@@ -38,6 +38,13 @@ export class HaChooseSelector extends LitElement {
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
if (
|
||||
changedProperties.has("value") &&
|
||||
changedProperties.get("value")?.active_choice &&
|
||||
changedProperties.get("value")?.active_choice !== this._activeChoice
|
||||
) {
|
||||
this._setActiveChoice();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
@@ -11,7 +12,10 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selector!: DurationSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: HaDurationData;
|
||||
@property({ attribute: false }) public value?:
|
||||
| HaDurationData
|
||||
| string
|
||||
| number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -21,16 +25,47 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
return { seconds: value };
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const negative = value.trim()[0] === "-";
|
||||
const parts = value
|
||||
.split(":")
|
||||
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
|
||||
|
||||
if (parts.length === 1) {
|
||||
return { seconds: parts[0] };
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return { hours: parts[0], minutes: parts[1] };
|
||||
}
|
||||
if (parts.length === 3) {
|
||||
return {
|
||||
hours: parts[0],
|
||||
minutes: parts[1],
|
||||
seconds: parts[2],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.data=${this.value}
|
||||
.data=${this._data(this.value)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.duration?.enable_day}
|
||||
.enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
.allowNegative=${this.selector.duration?.allow_negative}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,6 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
energy: 1,
|
||||
map: 2,
|
||||
@@ -344,17 +342,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
this._calculateCounts();
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
if (oldHass?.panelUrl !== this.hass.panelUrl) {
|
||||
const selectedEl = this.shadowRoot!.querySelector(".selected");
|
||||
if (selectedEl) {
|
||||
// @ts-ignore
|
||||
selectedEl.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _calculateCounts = throttle(() => {
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaSlider extends Slider {
|
||||
#thumb {
|
||||
border: none;
|
||||
background-color: var(--ha-slider-thumb-color, var(--primary-color));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#thumb:after {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import {
|
||||
@@ -49,7 +50,6 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-z-index - Z-index for the dialog.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
@@ -114,6 +114,8 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
@@ -139,6 +141,8 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
|
||||
)}
|
||||
aria-describedby=${ifDefined(this.ariaDescribedBy)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@wa-hide=${this._handleHide}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
@@ -208,9 +212,11 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = () => {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -223,6 +229,23 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleHide(ev: CustomEvent<{ source: Element }>) {
|
||||
if (
|
||||
this.preventScrimClose &&
|
||||
this._escapePressed &&
|
||||
ev.detail.source === (ev.target as WaDialog).dialog
|
||||
) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
@@ -381,7 +382,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
return `${path.name}<br>${formattedTime}`;
|
||||
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
|
||||
}
|
||||
|
||||
private _drawPaths(): void {
|
||||
@@ -549,7 +550,7 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
} else {
|
||||
const el = document.createElement("span");
|
||||
el.innerHTML = title;
|
||||
el.textContent = title;
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiAlphaABoxOutline,
|
||||
mdiArrowLeft,
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
mdiGrid,
|
||||
@@ -21,9 +20,10 @@ import type {
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog";
|
||||
import "../ha-wa-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-list-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
import "./ha-media-manage-button";
|
||||
import "./ha-media-player-browse";
|
||||
import type {
|
||||
@@ -44,6 +44,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
|
||||
|
||||
public showDialog(params: MediaPlayerBrowseDialogParams): void {
|
||||
@@ -54,9 +56,11 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
media_content_type: undefined,
|
||||
},
|
||||
];
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._navigateIds = undefined;
|
||||
this._currentItem = undefined;
|
||||
@@ -71,28 +75,20 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
.heading=${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._dialogOpened}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-dialog-header show-border slot="header">
|
||||
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
|
||||
? html`
|
||||
<ha-icon-button
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="navigationIcon"
|
||||
.path=${mdiArrowLeft}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button>
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: nothing}
|
||||
<span slot="title">
|
||||
@@ -153,7 +149,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
data-dialog="close"
|
||||
slot="actionItems"
|
||||
></ha-icon-button>
|
||||
</ha-dialog-header>
|
||||
@@ -173,7 +169,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -225,8 +221,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -241,9 +236,9 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-max-width: 800px;
|
||||
--ha-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiSpeaker } from "@mdi/js";
|
||||
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import "../ha-switch";
|
||||
import "../ha-svg-icon";
|
||||
import type { MediaPlayerEntity } from "../../data/media-player";
|
||||
|
||||
@customElement("ha-media-player-toggle")
|
||||
class HaMediaPlayerToggle extends LitElement {
|
||||
@@ -20,15 +21,61 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
isRTL: boolean,
|
||||
stateObj: HomeAssistant["states"][string]
|
||||
) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
|
||||
let icon = mdiSpeaker;
|
||||
if (stateObj.state === "playing") {
|
||||
icon = mdiSpeakerPlay;
|
||||
} else if (stateObj.state === "paused") {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
isRTL,
|
||||
stateObj
|
||||
);
|
||||
|
||||
return html`<div class="list-item">
|
||||
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
<div class="info">
|
||||
<div class="main-text">${computeStateName(stateObj)}</div>
|
||||
<div class="secondary-text">
|
||||
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
|
||||
</div>
|
||||
<div class="main-text">${primary}</div>
|
||||
<div class="secondary-text">${secondary}</div>
|
||||
</div>
|
||||
<ha-switch
|
||||
.disabled=${this.disabled}
|
||||
@@ -38,16 +85,6 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _formatSecondaryText(stateObj: MediaPlayerEntity): string {
|
||||
if (stateObj.state !== "playing") {
|
||||
return this.hass.localize("ui.card.media_player.idle");
|
||||
}
|
||||
|
||||
return [stateObj.attributes.media_title, stateObj.attributes.media_artist]
|
||||
.filter((segment) => segment)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
||||
51
src/components/voice-assistant-brand-icon.ts
Normal file
51
src/components/voice-assistant-brand-icon.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { voiceAssistants } from "../data/expose";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
|
||||
@customElement("voice-assistant-brand-icon")
|
||||
export class VoiceAssistantBrandicon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public voiceAssistantId!: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<img
|
||||
class="logo"
|
||||
alt=${voiceAssistants[this.voiceAssistantId].name}
|
||||
src=${brandsUrl({
|
||||
domain: voiceAssistants[this.voiceAssistantId].domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.logo {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"voice-assistant-brand-icon": VoiceAssistantBrandicon;
|
||||
}
|
||||
}
|
||||
@@ -449,16 +449,9 @@ const getEnergyData = async (
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
const period =
|
||||
isFirstDayOfMonth(start) &&
|
||||
(!end || isLastDayOfMonth(end)) &&
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
const finePeriod =
|
||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
|
||||
const period = getSuggestedPeriod(start, end);
|
||||
const finePeriod = getSuggestedPeriod(start, end, true);
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
@@ -589,7 +582,7 @@ const getEnergyData = async (
|
||||
consumptionStatIDs,
|
||||
co2SignalEntity,
|
||||
end,
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
period
|
||||
);
|
||||
if (compare) {
|
||||
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
|
||||
@@ -598,7 +591,7 @@ const getEnergyData = async (
|
||||
consumptionStatIDs,
|
||||
co2SignalEntity,
|
||||
endCompare,
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
period
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1427,3 +1420,22 @@ export const formatPowerShort = (
|
||||
units[unitIndex]
|
||||
);
|
||||
};
|
||||
|
||||
export function getSuggestedPeriod(
|
||||
start: Date,
|
||||
end?: Date,
|
||||
fine = false
|
||||
): "5minute" | "hour" | "day" | "month" {
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
|
||||
if (fine) {
|
||||
return dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
}
|
||||
return isFirstDayOfMonth(start) &&
|
||||
(!end || isLastDayOfMonth(end)) &&
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
|
||||
current_humidity: "%",
|
||||
min_humidity: "%",
|
||||
max_humidity: "%",
|
||||
target_humidity_step: "%",
|
||||
},
|
||||
light: {
|
||||
color_temp: "mired",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import { entityRegistryByEntityId } from "./entity/entity_registry";
|
||||
|
||||
export const voiceAssistants = {
|
||||
conversation: { domain: "assist_pipeline", name: "Assist" },
|
||||
@@ -52,3 +54,13 @@ export const listExposedEntities = (hass: HomeAssistant) =>
|
||||
hass.callWS<{ exposed_entities: Record<string, ExposeEntitySettings> }>({
|
||||
type: "homeassistant/expose_entity/list",
|
||||
});
|
||||
|
||||
export const getEntityVoiceAssistantsIds = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
entityId: string
|
||||
) => {
|
||||
const entity = entityRegistryByEntityId(entityRegistry)[entityId];
|
||||
return Object.keys(voiceAssistants).filter(
|
||||
(vaKey) => entity?.options?.[vaKey]?.should_expose
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export type HumidifierEntity = HassEntityBase & {
|
||||
mode?: string;
|
||||
action?: HumidifierAction;
|
||||
available_modes?: string[];
|
||||
target_humidity_step?: number;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ export interface BaseActionConfig {
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
title?: string;
|
||||
confirm_text?: string;
|
||||
dismiss_text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface LovelaceBaseViewConfig {
|
||||
title?: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
show_icon_and_title?: boolean;
|
||||
theme?: string;
|
||||
panel?: boolean;
|
||||
background?: string | LovelaceViewBackgroundConfig;
|
||||
|
||||
@@ -221,6 +221,7 @@ export interface DurationSelector {
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
allow_negative?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -376,7 +377,7 @@ interface SelectBoxOptionImage {
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: any;
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
image?: string | SelectBoxOptionImage;
|
||||
|
||||
@@ -44,14 +44,27 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
|
||||
entity.attributes.update_percentage !== null;
|
||||
|
||||
export const updateAvailable = (
|
||||
entity: UpdateEntity,
|
||||
showSkipped = false
|
||||
): boolean =>
|
||||
entity.state === BINARY_STATE_ON ||
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version));
|
||||
|
||||
export const updateCanInstall = (
|
||||
entity: UpdateEntity,
|
||||
showSkipped = false
|
||||
): boolean =>
|
||||
(entity.state === BINARY_STATE_ON ||
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
updateAvailable(entity, showSkipped) &&
|
||||
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||
|
||||
export const updateCanNotInstall = (
|
||||
entity: UpdateEntity,
|
||||
showSkipped = false
|
||||
): boolean =>
|
||||
updateAvailable(entity, showSkipped) &&
|
||||
!supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||
|
||||
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
|
||||
!!(
|
||||
entity.attributes.latest_version &&
|
||||
@@ -108,13 +121,17 @@ export const filterUpdateEntities = (
|
||||
);
|
||||
});
|
||||
|
||||
export const filterUpdateEntitiesWithInstall = (
|
||||
export const filterUpdateEntitiesParameterized = (
|
||||
entities: HassEntities,
|
||||
showSkipped = false
|
||||
showSkipped = false,
|
||||
showNotInstallable = false
|
||||
) =>
|
||||
filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
);
|
||||
filterUpdateEntities(entities).filter((entity) => {
|
||||
if (showNotInstallable) {
|
||||
return updateCanNotInstall(entity, showSkipped);
|
||||
}
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
|
||||
@@ -766,7 +766,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.content-wrapper.settings-view .fade-bottom {
|
||||
bottom: var(--ha-space-18);
|
||||
bottom: calc(
|
||||
var(--ha-space-14) +
|
||||
max(var(--safe-area-inset-bottom), var(--ha-space-4))
|
||||
);
|
||||
}
|
||||
|
||||
.child-view {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mdiAppleKeyboardCommand } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -154,6 +153,10 @@ const _SHORTCUTS: Section[] = [
|
||||
shortcut: ["M"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
|
||||
},
|
||||
{
|
||||
shortcut: ["Shift", "/"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.other.show_shortcuts",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -184,9 +187,7 @@ class DialogShortcuts extends LitElement {
|
||||
html`<span
|
||||
>${shortcutKey === CTRL_CMD
|
||||
? isMac
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
? "⌘"
|
||||
: this.hass.localize("ui.panel.config.automation.editor.ctrl")
|
||||
: typeof shortcutKey === "string"
|
||||
? shortcutKey
|
||||
|
||||
@@ -152,7 +152,7 @@ export const provideHass = (
|
||||
for (const ent of ensureArray(newEntities)) {
|
||||
hass().entities[ent.entityId] = {
|
||||
entity_id: ent.entityId,
|
||||
name: ent.name,
|
||||
name: ent.attributes.friendly_name || null,
|
||||
icon: ent.icon,
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
/**
|
||||
* Safe area padding in pixels for the scrollable element.
|
||||
*/
|
||||
protected scrollFadeSafeAreaPadding = 16;
|
||||
protected scrollFadeSafeAreaPadding = 4;
|
||||
|
||||
/**
|
||||
* Scroll threshold in pixels for showing the fades.
|
||||
@@ -73,6 +73,9 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated?.(changedProperties);
|
||||
if (this.scrollableElement) {
|
||||
this._updateScrollableState(this.scrollableElement);
|
||||
}
|
||||
this._attachScrollableElement();
|
||||
}
|
||||
|
||||
@@ -83,6 +86,8 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
|
||||
disconnectedCallback() {
|
||||
this._detachScrollableElement();
|
||||
this._contentScrolled = false;
|
||||
this._contentScrollable = false;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -125,16 +130,16 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--ha-space-4);
|
||||
height: var(--ha-space-2);
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--shadow-color),
|
||||
transparent
|
||||
);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--ha-color-shadow-scrollable-fade),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
.fade-top {
|
||||
top: 0;
|
||||
|
||||
@@ -58,6 +58,7 @@ import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type {
|
||||
Action,
|
||||
DeviceAction,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
@@ -233,6 +234,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const target =
|
||||
type === "service" && "target" in this.action
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
@@ -254,9 +262,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${type === "service" && "target" in this.action
|
||||
? this._renderTargets((this.action as ServiceAction).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -2062,6 +2062,7 @@ class DialogAddAutomationElement
|
||||
|
||||
.content.column {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
|
||||
@@ -1504,14 +1504,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.targets-show-more {
|
||||
box-shadow: inset var(--ha-shadow-offset-x-lg)
|
||||
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
|
||||
var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
|
||||
}
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px), all and (max-height: 500px) {
|
||||
|
||||
@@ -285,6 +285,8 @@ export class HaAutomationAddItems extends LitElement {
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
|
||||
border: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-quiet);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
`
|
||||
: nothing}
|
||||
${this._visibleOptionals.includes("description")
|
||||
? html` <ha-textarea
|
||||
? html`<ha-textarea
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.description.label"
|
||||
)}
|
||||
@@ -168,6 +168,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
autogrow
|
||||
.value=${this._newDescription}
|
||||
.helper=${supportsMarkdownHelper(this.hass.localize)}
|
||||
helperPersistent
|
||||
@input=${this._valueChanged}
|
||||
></ha-textarea>`
|
||||
: nothing}
|
||||
@@ -570,7 +571,7 @@ ${dump(this._params.config)}
|
||||
ha-category-picker,
|
||||
ha-labels-picker,
|
||||
ha-area-picker,
|
||||
ha-chip-set {
|
||||
ha-chip-set:has(> ha-assist-chip) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-alert {
|
||||
|
||||
@@ -76,6 +76,7 @@ import "./types/ha-automation-condition-template";
|
||||
import "./types/ha-automation-condition-time";
|
||||
import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import type { DeviceCondition } from "../../../../data/device/device_automation";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -184,6 +185,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const target =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {})
|
||||
? (this.condition as PlatformCondition).target
|
||||
: "device_id" in this.condition &&
|
||||
(this.condition as DeviceCondition).device_id
|
||||
? { device_id: [(this.condition as DeviceCondition).device_id] }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
@@ -194,10 +203,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${"target" in
|
||||
(this.conditionDescriptions[this.condition.condition] || {})
|
||||
? this._renderTargets((this.condition as PlatformCondition).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -57,6 +57,7 @@ import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu";
|
||||
@@ -115,6 +116,8 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import "../voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
@@ -376,6 +379,31 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
></ha-icon-button>
|
||||
`,
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (automation) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
automation.entity_id
|
||||
);
|
||||
return html` ${exposedToVoiceAssistantIds.length !== 0
|
||||
? exposedToVoiceAssistantIds.map(
|
||||
(vaId) =>
|
||||
html` <voice-assistants-expose-assistant-icon
|
||||
.assistant=${vaId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistants-expose-assistant-icon>`
|
||||
)
|
||||
: "—"}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
@@ -633,6 +661,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-voice-assistants>
|
||||
<ha-filter-blueprints
|
||||
.hass=${this.hass}
|
||||
.type=${"automation"}
|
||||
@@ -1003,8 +1040,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(categoryItems)
|
||||
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||
}
|
||||
if (
|
||||
} else if (
|
||||
key === "ha-filter-labels" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
@@ -1026,6 +1062,29 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(labelItems)
|
||||
: new Set([...items].filter((x) => labelItems!.has(x)));
|
||||
} else if (
|
||||
key === "ha-filter-voice-assistants" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
const assistItems = new Set<string>();
|
||||
this.automations
|
||||
.filter((automation) =>
|
||||
getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
automation.entity_id
|
||||
).some((va) => (filter.value as string[]).includes(va))
|
||||
)
|
||||
.forEach((automation) => assistItems.add(automation.entity_id));
|
||||
if (!items) {
|
||||
items = assistItems;
|
||||
continue;
|
||||
}
|
||||
items =
|
||||
"intersection" in items
|
||||
? // @ts-ignore
|
||||
items.intersection(assistItems)
|
||||
: new Set([...items].filter((x) => assistItems!.has(x)));
|
||||
}
|
||||
}
|
||||
this._filteredAutomations = items ? [...items] : undefined;
|
||||
|
||||
@@ -223,6 +223,8 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
border: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-quiet);
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
|
||||
import { describeTrigger } from "../../../../data/automation_i18n";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { DeviceTrigger } from "../../../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
@@ -196,6 +197,15 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
const yamlMode = this._yamlMode || !supported;
|
||||
|
||||
const target =
|
||||
type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? (this.trigger as PlatformTrigger).target
|
||||
: type === "device" && (this.trigger as DeviceTrigger).device_id
|
||||
? { device_id: (this.trigger as DeviceTrigger).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "list"
|
||||
? html`<ha-svg-icon
|
||||
@@ -210,11 +220,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? this._renderTargets((this.trigger as PlatformTrigger).target)
|
||||
: nothing}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
@@ -206,8 +206,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
sortable: true,
|
||||
valueColumn: "usageCount",
|
||||
type: "numeric",
|
||||
minWidth: "100px",
|
||||
maxWidth: "120px",
|
||||
minWidth: "90px",
|
||||
maxWidth: "90px",
|
||||
template: (blueprint) => {
|
||||
const count = blueprint.usageCount ?? 0;
|
||||
return html`
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
|
||||
|
||||
@customElement("ha-category-picker")
|
||||
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -101,17 +100,11 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _getCategories = memoizeOne(
|
||||
(categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return [
|
||||
{
|
||||
id: NO_CATEGORIES_ID,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.category-picker.no_categories"
|
||||
),
|
||||
icon_path: mdiTag,
|
||||
},
|
||||
];
|
||||
(
|
||||
categories: CategoryRegistryEntry[] | undefined
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!categories) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = categories.map<PickerComboBoxItem>((category) => ({
|
||||
@@ -210,10 +203,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_CATEGORIES_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiLocationEnter,
|
||||
mdiLocationExit,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
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 { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
HassioSupervisorInfo,
|
||||
@@ -26,13 +23,16 @@ import {
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
filterUpdateEntitiesParameterized,
|
||||
} from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@@ -53,7 +53,11 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
const canInstallUpdates = this._filterInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
@@ -73,57 +77,86 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu multi>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
.checked=${this._showSkipped}
|
||||
value="show_skipped"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
</ha-dropdown-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._supervisorInfo.channel === "stable"
|
||||
? mdiLocationEnter
|
||||
: mdiLocationExit}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.updates.${this._supervisorInfo.channel === "stable" ? "join" : "leave"}_beta`
|
||||
)}
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.isInstallable=${true}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
`
|
||||
: html`
|
||||
<div class="no-updates">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.no_updates"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${notInstallableUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${notInstallableUpdates}
|
||||
.isInstallable=${false}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${canInstallUpdates.length + notInstallableUpdates.length
|
||||
? nothing
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
<div class="no-updates">
|
||||
${this.hass.localize("ui.panel.config.updates.no_updates")}
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -133,27 +166,19 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
|
||||
private async _toggleBeta(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
private _handleOverflowAction(
|
||||
ev: CustomEvent<{ item: { value: string } }>
|
||||
): void {
|
||||
if (ev.detail.item.value === "toggle_beta") {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
}
|
||||
} else if (ev.detail.item.value === "show_skipped") {
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,9 +202,14 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
private _filterInstallableUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesWithInstall(entities, showSkipped)
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, false)
|
||||
);
|
||||
|
||||
private _filterNotInstallableUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, true)
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
filterUpdateEntitiesParameterized,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
QuickBarMode,
|
||||
@@ -161,24 +161,27 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => [
|
||||
isCloudLoaded
|
||||
? [
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
translationKey: "cloud",
|
||||
},
|
||||
...configSections.dashboard,
|
||||
]
|
||||
: configSections.dashboard,
|
||||
configSections.dashboard_2,
|
||||
configSections.dashboard_3,
|
||||
]);
|
||||
private _pages = memoizeOne(
|
||||
(cloudStatus, isCloudLoaded, hasExternalSettings) => [
|
||||
isCloudLoaded
|
||||
? [
|
||||
{
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
translationKey: "cloud",
|
||||
},
|
||||
...configSections.dashboard,
|
||||
]
|
||||
: configSections.dashboard,
|
||||
hasExternalSettings ? configSections.dashboard_external_settings : [],
|
||||
configSections.dashboard_2,
|
||||
configSections.dashboard_3,
|
||||
]
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
@@ -203,7 +206,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesWithInstall(
|
||||
this._filterUpdateEntitiesParameterized(
|
||||
this.hass.states,
|
||||
this.hass.entities
|
||||
);
|
||||
@@ -288,6 +291,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
.total=${totalUpdates}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.isInstallable=${true}
|
||||
></ha-config-updates>
|
||||
${totalUpdates > canInstallUpdates.length
|
||||
? html`
|
||||
@@ -310,7 +314,8 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
${this._pages(
|
||||
this.cloudStatus,
|
||||
isComponentLoaded(this.hass, "cloud")
|
||||
isComponentLoaded(this.hass, "cloud"),
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
).map((categoryPages) =>
|
||||
categoryPages.length === 0
|
||||
? nothing
|
||||
@@ -344,14 +349,16 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
showShortcutsDialog(this);
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
private _filterUpdateEntitiesParameterized = memoizeOne(
|
||||
(
|
||||
entities: HomeAssistant["states"],
|
||||
entityRegistry: HomeAssistant["entities"]
|
||||
): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities).filter(
|
||||
(entity) => !entityRegistry[entity.entity_id]?.hidden
|
||||
);
|
||||
const updates = filterUpdateEntitiesParameterized(
|
||||
entities,
|
||||
false,
|
||||
false
|
||||
).filter((entity) => !entityRegistry[entity.entity_id]?.hidden);
|
||||
|
||||
return {
|
||||
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
|
||||
@@ -32,6 +32,8 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Number }) public total?: number;
|
||||
|
||||
@property({ attribute: false }) public isInstallable = true;
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
@@ -89,9 +91,16 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.total || this.updateEntities.length,
|
||||
})}
|
||||
${this.isInstallable
|
||||
? this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: this.total || this.updateEntities.length,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: this.total || this.updateEntities.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${updates.map((entity) => {
|
||||
|
||||
@@ -64,6 +64,7 @@ import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
@@ -115,6 +116,8 @@ import { isHelperDomain } from "../helpers/const";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import "../voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
export interface StateEntity extends Omit<
|
||||
EntityRegistryEntry,
|
||||
@@ -493,6 +496,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
template: (entry) =>
|
||||
entry.label_entries.map((lbl) => lbl.name).join(" "),
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (entry) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entities,
|
||||
entry.entity_id
|
||||
);
|
||||
return html` ${exposedToVoiceAssistantIds.length !== 0
|
||||
? exposedToVoiceAssistantIds.map(
|
||||
(vaId) =>
|
||||
html` <voice-assistants-expose-assistant-icon
|
||||
.assistant=${vaId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistants-expose-assistant-icon>`
|
||||
)
|
||||
: "—"}`;
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -637,6 +665,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
entity.labels.some((lbl) => (filter as string[]).includes(lbl))
|
||||
);
|
||||
} else if (
|
||||
key === "ha-filter-voice-assistants" &&
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
getEntityVoiceAssistantsIds(this._entities, entity.entity_id).some(
|
||||
(va) => (filter as string[]).includes(va)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1076,6 +1114,15 @@ ${
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-labels>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-voice-assistants>
|
||||
${
|
||||
includeAddDeviceFab
|
||||
? html`<ha-fab
|
||||
@@ -1128,6 +1175,7 @@ ${
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.get("label");
|
||||
const voiceAssistant = this._searchParms.get("voice_assistant");
|
||||
|
||||
if (!domain && !configEntry && !label && !device) {
|
||||
return;
|
||||
@@ -1140,6 +1188,7 @@ ${
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
|
||||
@@ -105,7 +105,24 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconColor: "#3263C3",
|
||||
},
|
||||
],
|
||||
dashboard_external_settings: [
|
||||
{
|
||||
path: "#external-app-configuration",
|
||||
translationKey: "companion",
|
||||
iconPath: mdiCellphoneCog,
|
||||
iconColor: "#8E24AA",
|
||||
},
|
||||
],
|
||||
dashboard_2: [
|
||||
{
|
||||
path: "/config/matter",
|
||||
name: "Matter",
|
||||
iconPath:
|
||||
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
|
||||
iconColor: "#2458B3",
|
||||
component: "matter",
|
||||
translationKey: "matter",
|
||||
},
|
||||
{
|
||||
path: "/config/zha",
|
||||
name: "Zigbee",
|
||||
@@ -173,12 +190,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconColor: "#5A87FA",
|
||||
component: ["person", "users"],
|
||||
},
|
||||
{
|
||||
path: "#external-app-configuration",
|
||||
translationKey: "companion",
|
||||
iconPath: mdiCellphoneCog,
|
||||
iconColor: "#8E24AA",
|
||||
},
|
||||
{
|
||||
path: "/config/system",
|
||||
translationKey: "system",
|
||||
|
||||
@@ -260,8 +260,6 @@ export class DialogHelperDetail extends LitElement {
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
class=${classMap({ "button-left": !this._domain })}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.hideActions=${!this._domain}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
|
||||
@@ -105,7 +105,7 @@ class HaTimerForm extends LitElement {
|
||||
<ha-checkbox
|
||||
.configValue=${"restore"}
|
||||
.checked=${this._restore}
|
||||
@click=${this._toggleRestore}
|
||||
@change=${this._toggleRestore}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-checkbox>
|
||||
@@ -135,11 +135,8 @@ class HaTimerForm extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleRestore() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._restore = !this._restore;
|
||||
private _toggleRestore(ev) {
|
||||
this._restore = ev.target.checked;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this._item, restore: this._restore },
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
@@ -122,6 +123,8 @@ import "../integrations/ha-integration-overflow-menu";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { isHelperDomain, type HelperDomain } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import "../voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
interface HelperItem {
|
||||
id: string;
|
||||
@@ -205,7 +208,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
@state() private _helperEntities: HassEntity[] = [];
|
||||
|
||||
@state() private _disabledEntityEntries?: EntityRegistryEntry[];
|
||||
|
||||
@@ -223,6 +226,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "helpers-table-filters",
|
||||
@@ -245,7 +250,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state() private _filteredStateItems?: string[] | null;
|
||||
@state() private _filteredHelperEntityIds?: string[] | null;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
@@ -480,6 +485,32 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (helper) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
helper.entity_id
|
||||
);
|
||||
return html` ${exposedToVoiceAssistantIds.length !== 0
|
||||
? exposedToVoiceAssistantIds.map(
|
||||
(vaId) => html`
|
||||
<voice-assistants-expose-assistant-icon
|
||||
.assistant=${vaId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistants-expose-assistant-icon>
|
||||
`
|
||||
)
|
||||
: "—"}`;
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -610,7 +641,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._stateItems === undefined ||
|
||||
this._helperEntities === undefined ||
|
||||
this._entityEntries === undefined ||
|
||||
this._configEntries === undefined
|
||||
) {
|
||||
@@ -685,14 +716,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
const helpers = this._getItems(
|
||||
this.hass.localize,
|
||||
this._stateItems,
|
||||
this._helperEntities,
|
||||
this._disabledEntityEntries || [],
|
||||
this._entityEntries,
|
||||
this._configEntries,
|
||||
this._entityReg,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredStateItems
|
||||
this._filteredHelperEntityIds
|
||||
);
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
@@ -779,6 +810,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-voice-assistants>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
@@ -941,7 +981,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filter.length
|
||||
) {
|
||||
const labelItems = new Set<string>();
|
||||
this._stateItems
|
||||
this._helperEntities
|
||||
.filter((stateItem) =>
|
||||
entityRegistryByEntityId(this._entityReg)[
|
||||
stateItem.entity_id
|
||||
@@ -960,14 +1000,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(labelItems)
|
||||
: new Set([...items].filter((x) => labelItems!.has(x)));
|
||||
}
|
||||
if (
|
||||
} else if (
|
||||
key === "ha-filter-categories" &&
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
const categoryItems = new Set<string>();
|
||||
this._stateItems
|
||||
this._helperEntities
|
||||
.filter(
|
||||
(stateItem) =>
|
||||
filter[0] ===
|
||||
@@ -987,10 +1026,85 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(categoryItems)
|
||||
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||
} else if (
|
||||
key === "ha-filter-voice-assistants" &&
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
const assistItems = new Set<string>();
|
||||
this._helperEntities
|
||||
.filter((stateItem) =>
|
||||
getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
stateItem.entity_id
|
||||
).some((va) => (filter as string[]).includes(va))
|
||||
)
|
||||
.forEach((stateItem) => assistItems.add(stateItem.entity_id));
|
||||
(this._disabledEntityEntries || [])
|
||||
.filter((entry) =>
|
||||
getEntityVoiceAssistantsIds(this._entityReg, entry.entity_id).some(
|
||||
(va) => (filter as string[]).includes(va)
|
||||
)
|
||||
)
|
||||
.forEach((entry) => assistItems.add(entry.entity_id));
|
||||
if (!items) {
|
||||
items = assistItems;
|
||||
continue;
|
||||
}
|
||||
items =
|
||||
"intersection" in items
|
||||
? // @ts-ignore
|
||||
items.intersection(assistItems)
|
||||
: new Set([...items].filter((x) => assistItems!.has(x)));
|
||||
}
|
||||
}
|
||||
this._filteredHelperEntityIds = items ? [...items] : undefined;
|
||||
}
|
||||
|
||||
this._filteredStateItems = items ? [...items] : undefined;
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _popState = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._filter = history.state?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
"ha-filter-categories": category ? [category] : [],
|
||||
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
|
||||
};
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
@@ -1093,7 +1207,7 @@ ${rejected
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._setFiltersFromUrl();
|
||||
this._fetchEntitySources();
|
||||
|
||||
if (isComponentLoaded(this.hass, "diagnostics")) {
|
||||
@@ -1206,6 +1320,10 @@ ${rejected
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
|
||||
if (!this._entityEntries || !this._configEntries) {
|
||||
return;
|
||||
}
|
||||
@@ -1225,7 +1343,7 @@ ${rejected
|
||||
}
|
||||
|
||||
let changed =
|
||||
!this._stateItems ||
|
||||
!this._helperEntities ||
|
||||
changedProps.has("_entityEntries") ||
|
||||
changedProps.has("_configEntries") ||
|
||||
changedProps.has("_entitySource");
|
||||
@@ -1240,17 +1358,17 @@ ${rejected
|
||||
|
||||
const entityIds = Object.keys(this._entitySource);
|
||||
|
||||
const newStates = Object.values(this.hass!.states).filter(
|
||||
const newHelpers = Object.values(this.hass!.states).filter(
|
||||
(entity) =>
|
||||
entityIds.includes(entity.entity_id) ||
|
||||
isHelperDomain(computeStateDomain(entity))
|
||||
);
|
||||
|
||||
if (
|
||||
this._stateItems.length !== newStates.length ||
|
||||
!this._stateItems.every((val, idx) => newStates[idx] === val)
|
||||
this._helperEntities.length !== newHelpers.length ||
|
||||
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
|
||||
) {
|
||||
this._stateItems = newStates;
|
||||
this._helperEntities = newHelpers;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
return html`<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { copyToClipboard } from "../../../common/util/copy-clipboard";
|
||||
@@ -213,10 +212,7 @@ class HaConfigEntryRow extends LitElement {
|
||||
? html`<ha-button slot="end" @click=${this._handleEnable}>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-button>`
|
||||
: configPanel &&
|
||||
(item.domain !== "matter" ||
|
||||
isDevVersion(this.hass.config.version)) &&
|
||||
!stateText
|
||||
: configPanel && !stateText
|
||||
? html`<a
|
||||
slot="end"
|
||||
href=${`/${configPanel}?config_entry=${item.entry_id}`}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiPlus } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
import "../../../../../components/ha-fab";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
acceptSharedMatterDevice,
|
||||
canCommissionMatterExternal,
|
||||
@@ -18,7 +26,6 @@ import {
|
||||
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@customElement("matter-config-dashboard")
|
||||
export class MatterConfigDashboard extends LitElement {
|
||||
@@ -26,6 +33,8 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _configEntry?: ConfigEntry;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsub?: UnsubscribeFunc;
|
||||
@@ -35,10 +44,33 @@ export class MatterConfigDashboard extends LitElement {
|
||||
this._stopRedirect();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this._fetchConfigEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private _matterDeviceCount = memoizeOne(
|
||||
(devices: HomeAssistant["devices"]): number =>
|
||||
Object.values(devices).filter((device) =>
|
||||
device.identifiers.some((identifier) => identifier[0] === "matter")
|
||||
).length
|
||||
);
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._configEntry) {
|
||||
return nothing;
|
||||
}
|
||||
const isOnline = this._configEntry.state === "loaded";
|
||||
return html`
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
|
||||
${isComponentLoaded(this.hass, "otbr")
|
||||
<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
header="Matter"
|
||||
has-fab
|
||||
>
|
||||
${isComponentLoaded(this.hass, "thread")
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -51,53 +83,114 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}</ha-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<ha-card header="Matter">
|
||||
<ha-alert alert-type="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.experimental_note"
|
||||
)}</ha-alert
|
||||
>
|
||||
: nothing}
|
||||
<div class="container">
|
||||
<ha-card class="network-status">
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
|
||||
<div class="heading">
|
||||
<div class="icon">
|
||||
<ha-svg-icon
|
||||
.path=${isOnline ? mdiCheckCircle : mdiAlertCircle}
|
||||
class=${isOnline ? "online" : "offline"}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<div class="details">
|
||||
Matter
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.status_title"
|
||||
)}:
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.matter.panel.status_${isOnline ? "online" : "offline"}`
|
||||
)}<br />
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.devices",
|
||||
{ count: this._matterDeviceCount(this.hass.devices) }
|
||||
)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._startMobileCommissioning}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
)}</ha-button
|
||||
>`
|
||||
: ""}
|
||||
<ha-button appearance="plain" @click=${this._commission}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}</ha-button
|
||||
<ha-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setWifi}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setThread}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
)}</ha-button
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.developer_tools_title"
|
||||
)}
|
||||
.secondary=${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.developer_tools_description"
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<div class="dev-tools-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.developer_tools_info"
|
||||
)}
|
||||
</p>
|
||||
<div class="dev-tools-actions">
|
||||
${canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._startMobileCommissioning}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
)}</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
<ha-button appearance="plain" @click=${this._commission}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setWifi}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setThread}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</div>
|
||||
|
||||
<a href="/config/matter/add" slot="fab">
|
||||
<ha-fab
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_device"
|
||||
)}
|
||||
extended
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</a>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
@@ -236,27 +329,101 @@ export class MatterConfigDashboard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
ha-alert[alert-type="warning"] {
|
||||
position: relative;
|
||||
top: -16px;
|
||||
}
|
||||
.content {
|
||||
padding: 24px 0 32px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-card:first-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
a[slot="toolbar-icon"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
private async _fetchConfigEntry(): Promise<void> {
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "matter",
|
||||
});
|
||||
if (configEntries.length) {
|
||||
this._configEntry = configEntries[0];
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-card {
|
||||
margin: auto;
|
||||
margin-top: var(--ha-space-4);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
ha-card .card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.network-status div.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.network-status div.heading .icon {
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.network-status div.heading ha-svg-icon {
|
||||
--mdc-icon-size: 48px;
|
||||
}
|
||||
|
||||
.network-status div.heading .details {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
}
|
||||
|
||||
.network-status small {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
.network-status .online {
|
||||
color: var(--state-on-color, var(--success-color));
|
||||
}
|
||||
|
||||
.network-status .offline {
|
||||
color: var(--error-color, var(--error-color));
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
margin: auto;
|
||||
margin-top: var(--ha-space-4);
|
||||
max-width: 500px;
|
||||
background: var(--card-background-color);
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
--expansion-panel-summary-padding: var(--ha-space-2) var(--ha-space-4);
|
||||
--expansion-panel-content-padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.dev-tools-content {
|
||||
padding: 0 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.dev-tools-content p {
|
||||
margin: 0 0 var(--ha-space-4);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.dev-tools-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
a[slot="toolbar-icon"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a[slot="fab"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -45,9 +45,9 @@ class DialogThreadDataset extends LitElement implements HassDialog {
|
||||
<div>
|
||||
Network name: ${dataset.network_name}<br />
|
||||
Channel: ${dataset.channel}<br />
|
||||
Dataset id: ${dataset.dataset_id}<br />
|
||||
Pan id: ${dataset.pan_id}<br />
|
||||
Extended Pan id: ${dataset.extended_pan_id}<br />
|
||||
Dataset ID: ${dataset.dataset_id}<br />
|
||||
PAN ID: ${dataset.pan_id}<br />
|
||||
Extended PAN ID: ${dataset.extended_pan_id}<br />
|
||||
|
||||
${hasOTBR
|
||||
? html`OTBR URL: ${otbrInfo.url}<br />
|
||||
|
||||
@@ -372,7 +372,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-generic-picker {
|
||||
max-width: 50%;
|
||||
max-width: max(30%, 160px);
|
||||
}
|
||||
ha-button {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -51,6 +51,7 @@ import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
@@ -107,6 +108,8 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import "../voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
type SceneItem = SceneEntity & {
|
||||
name: string;
|
||||
@@ -410,6 +413,31 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (scene) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
scene.entity_id
|
||||
);
|
||||
return html` ${exposedToVoiceAssistantIds.length !== 0
|
||||
? exposedToVoiceAssistantIds.map(
|
||||
(vaId) =>
|
||||
html` <voice-assistants-expose-assistant-icon
|
||||
.assistant=${vaId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistants-expose-assistant-icon>`
|
||||
)
|
||||
: "—"}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
@@ -652,6 +680,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-voice-assistants>
|
||||
|
||||
${!this.narrow
|
||||
? html`<ha-md-button-menu slot="selection-bar">
|
||||
@@ -887,8 +924,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(categoryItems)
|
||||
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||
}
|
||||
if (
|
||||
} else if (
|
||||
key === "ha-filter-labels" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
@@ -910,6 +946,28 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(labelItems)
|
||||
: new Set([...items].filter((x) => labelItems!.has(x)));
|
||||
} else if (
|
||||
key === "ha-filter-voice-assistants" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
const assistItems = new Set<string>();
|
||||
this.scenes
|
||||
.filter((scene) =>
|
||||
getEntityVoiceAssistantsIds(this._entityReg, scene.entity_id).some(
|
||||
(va) => (filter.value as string[]).includes(va)
|
||||
)
|
||||
)
|
||||
.forEach((scene) => assistItems.add(scene.entity_id));
|
||||
if (!items) {
|
||||
items = assistItems;
|
||||
continue;
|
||||
}
|
||||
items =
|
||||
"intersection" in items
|
||||
? // @ts-ignore
|
||||
items.intersection(assistItems)
|
||||
: new Set([...items].filter((x) => assistItems!.has(x)));
|
||||
}
|
||||
}
|
||||
this._filteredScenes = items ? [...items] : undefined;
|
||||
|
||||
@@ -53,6 +53,7 @@ import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-entities";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-labels";
|
||||
import "../../../components/ha-filter-voice-assistants";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-md-divider";
|
||||
@@ -111,6 +112,8 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import "../voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
type ScriptItem = ScriptEntity & {
|
||||
name: string;
|
||||
@@ -398,8 +401,32 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
voice_assistants: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
type: "flex",
|
||||
defaultHidden: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
template: (script) => {
|
||||
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
|
||||
this._entityReg,
|
||||
script.entity_id
|
||||
);
|
||||
return html` ${exposedToVoiceAssistantIds.length !== 0
|
||||
? exposedToVoiceAssistantIds.map(
|
||||
(vaId) =>
|
||||
html` <voice-assistants-expose-assistant-icon
|
||||
.assistant=${vaId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistants-expose-assistant-icon>`
|
||||
)
|
||||
: "—"}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
@@ -635,6 +662,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-categories>
|
||||
<ha-filter-voice-assistants
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-voice-assistants"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-voice-assistants>
|
||||
<ha-filter-blueprints
|
||||
.hass=${this.hass}
|
||||
.type=${"script"}
|
||||
@@ -893,8 +929,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(categoryItems)
|
||||
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||
}
|
||||
if (
|
||||
} else if (
|
||||
key === "ha-filter-labels" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
@@ -916,6 +951,28 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(labelItems)
|
||||
: new Set([...items].filter((x) => labelItems!.has(x)));
|
||||
} else if (
|
||||
key === "ha-filter-voice-assistants" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
const assistItems = new Set<string>();
|
||||
this.scripts
|
||||
.filter((script) =>
|
||||
getEntityVoiceAssistantsIds(this._entityReg, script.entity_id).some(
|
||||
(va) => (filter.value as string[]).includes(va)
|
||||
)
|
||||
)
|
||||
.forEach((script) => assistItems.add(script.entity_id));
|
||||
if (!items) {
|
||||
items = assistItems;
|
||||
continue;
|
||||
}
|
||||
items =
|
||||
"intersection" in items
|
||||
? // @ts-ignore
|
||||
items.intersection(assistItems)
|
||||
: new Set([...items].filter((x) => assistItems!.has(x)));
|
||||
}
|
||||
}
|
||||
this._filteredScripts = items ? [...items] : undefined;
|
||||
|
||||
@@ -23,7 +23,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
|
||||
|
||||
render() {
|
||||
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
|
||||
|
||||
return html`
|
||||
<div class="container" id="container">
|
||||
<img
|
||||
|
||||
@@ -21,6 +21,8 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
@state() private _subscribed?: () => void;
|
||||
|
||||
@state() private _eventFilter = "";
|
||||
|
||||
@state() private _events: {
|
||||
id: number;
|
||||
event: HassEvent;
|
||||
@@ -30,6 +32,8 @@ class EventSubscribeCard extends LitElement {
|
||||
|
||||
private _eventCount = 0;
|
||||
|
||||
@state() _ignoredEventsCount = 0;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._subscribed) {
|
||||
@@ -70,6 +74,16 @@ class EventSubscribeCard extends LitElement {
|
||||
.value=${this._eventType}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.developer-tools.tabs.events.filter_events"
|
||||
)}
|
||||
.value=${this._eventFilter}
|
||||
.disabled=${this._subscribed !== undefined}
|
||||
helperPersistent
|
||||
.helper=${`${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
@@ -135,6 +149,46 @@ class EventSubscribeCard extends LitElement {
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _filterChanged(ev): void {
|
||||
this._eventFilter = ev.target.value;
|
||||
}
|
||||
|
||||
private _testEventFilter(event: HassEvent): boolean {
|
||||
if (!this._eventFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const searchStr = this._eventFilter;
|
||||
|
||||
function visit(node) {
|
||||
// Handle primitives directly
|
||||
if (node === null || typeof node !== "object") {
|
||||
return String(node).includes(searchStr);
|
||||
}
|
||||
|
||||
// Handle arrays and plain objects
|
||||
for (const key in node) {
|
||||
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
|
||||
// Check key
|
||||
if (key.includes(searchStr)) return true;
|
||||
|
||||
const value = node[key];
|
||||
|
||||
// Check primitive value
|
||||
if (value === null || typeof value !== "object") {
|
||||
if (String(value).includes(searchStr)) return true;
|
||||
} else if (visit(value)) {
|
||||
// Recurse into nested object/array
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return visit(event);
|
||||
}
|
||||
|
||||
private async _startOrStopListening(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
this._subscribed();
|
||||
@@ -144,6 +198,10 @@ class EventSubscribeCard extends LitElement {
|
||||
try {
|
||||
this._subscribed =
|
||||
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
|
||||
if (!this._testEventFilter(event)) {
|
||||
this._ignoredEventsCount++;
|
||||
return;
|
||||
}
|
||||
const tail =
|
||||
this._events.length > 30
|
||||
? this._events.slice(0, 29)
|
||||
@@ -168,6 +226,7 @@ class EventSubscribeCard extends LitElement {
|
||||
private _clearEvents(): void {
|
||||
this._events = [];
|
||||
this._eventCount = 0;
|
||||
this._ignoredEventsCount = 0;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
label: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
|
||||
),
|
||||
type: "icon",
|
||||
template: (statistic) =>
|
||||
html`${statistic.issues
|
||||
? html`<ha-button
|
||||
|
||||
@@ -283,13 +283,18 @@ class PanelEnergy extends LitElement {
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
|
||||
const hasPower =
|
||||
this._prefs.energy_sources.some(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
) || this._prefs.device_consumption.some((device) => device.stat_rate);
|
||||
const hasPowerSource = this._prefs.energy_sources.some(
|
||||
(source) =>
|
||||
(source.type === "solar" && source.stat_rate) ||
|
||||
(source.type === "battery" && source.stat_rate) ||
|
||||
(source.type === "grid" && source.power?.length)
|
||||
);
|
||||
|
||||
const hasDevicePower = this._prefs.device_consumption.some(
|
||||
(device) => device.stat_rate
|
||||
);
|
||||
|
||||
const hasPower = hasPowerSource || hasDevicePower;
|
||||
|
||||
const hasWater =
|
||||
this._prefs.energy_sources.some((source) => source.type === "water") ||
|
||||
@@ -314,7 +319,10 @@ class PanelEnergy extends LitElement {
|
||||
if (hasPower) {
|
||||
views.push(POWER_VIEW);
|
||||
}
|
||||
if (views.length > 1) {
|
||||
if (
|
||||
hasPowerSource ||
|
||||
[hasEnergy, hasGas, hasWater].filter(Boolean).length > 1
|
||||
) {
|
||||
views.unshift(OVERVIEW_VIEW);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { StatisticPeriod } from "../../../../../data/recorder";
|
||||
import { getSuggestedPeriod } from "../../../../../data/energy";
|
||||
|
||||
export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
|
||||
let suggestedMax = new Date(end);
|
||||
@@ -56,10 +57,6 @@ export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
|
||||
export function getSuggestedPeriod(dayDifference: number): StatisticPeriod {
|
||||
return dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||
}
|
||||
|
||||
function createYAxisLabelFormatter(locale: FrontendLocaleData) {
|
||||
let previousValue: number | undefined;
|
||||
|
||||
@@ -95,7 +92,7 @@ export function getCommonOptions(
|
||||
type: "time",
|
||||
min: start,
|
||||
max: getSuggestedMax(
|
||||
detailedDailyData ? "5minute" : getSuggestedPeriod(dayDifference),
|
||||
getSuggestedPeriod(start, end, detailedDailyData),
|
||||
end
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { differenceInDays, endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import { endOfToday, isToday, startOfToday } from "date-fns";
|
||||
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getEnergySolarForecasts,
|
||||
getSuggestedPeriod,
|
||||
} from "../../../../data/energy";
|
||||
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
|
||||
import { getStatisticLabel } from "../../../../data/recorder";
|
||||
@@ -354,7 +355,7 @@ export class HuiEnergySolarGraphCard
|
||||
) {
|
||||
const data: LineSeriesOption[] = [];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
const period = getSuggestedPeriod(start, end);
|
||||
|
||||
// Process solar forecast data.
|
||||
solarSources.forEach((source) => {
|
||||
@@ -370,10 +371,10 @@ export class HuiEnergySolarGraphCard
|
||||
if (dateObj < start || (end && dateObj > end)) {
|
||||
return;
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
if (period === "month") {
|
||||
dateObj.setDate(1);
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
if (period === "month" || period === "day") {
|
||||
dateObj.setHours(0, 0, 0, 0);
|
||||
} else {
|
||||
dateObj.setMinutes(0, 0, 0);
|
||||
|
||||
@@ -67,6 +67,19 @@ export const SUM_DEVICE_CLASSES = [
|
||||
"water",
|
||||
];
|
||||
|
||||
// Additional sources for sensor device classes from entity attributes
|
||||
// Maps device_class -> array of { domain, attribute } to include in aggregation
|
||||
export const SENSOR_ATTRIBUTE_SOURCES: Record<
|
||||
string,
|
||||
{ domain: string; attribute: string }[]
|
||||
> = {
|
||||
temperature: [{ domain: "climate", attribute: "current_temperature" }],
|
||||
humidity: [
|
||||
{ domain: "climate", attribute: "current_humidity" },
|
||||
{ domain: "humidifier", attribute: "current_humidity" },
|
||||
],
|
||||
};
|
||||
|
||||
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
|
||||
exclude_entities?: string[];
|
||||
}
|
||||
@@ -251,6 +264,24 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
private _domainEntityIds = memoizeOne(
|
||||
(
|
||||
entities: HomeAssistant["entities"],
|
||||
areaId: string,
|
||||
domains: string[],
|
||||
excludeEntities?: string[]
|
||||
): string[] => {
|
||||
const filter = generateEntityFilter(this.hass, {
|
||||
area: areaId,
|
||||
entity_category: "none",
|
||||
domain: domains,
|
||||
});
|
||||
return Object.keys(entities).filter(
|
||||
(id) => filter(id) && !excludeEntities?.includes(id)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _computeActiveAlertStates(): HassEntity[] {
|
||||
const areaId = this._config?.area;
|
||||
const area = areaId ? this.hass.areas[areaId] : undefined;
|
||||
@@ -359,58 +390,91 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
: this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
|
||||
const entityIds = groupedEntities.get(sensorClass);
|
||||
const sensorEntityIds = groupedEntities.get(sensorClass) || [];
|
||||
const values: number[] = [];
|
||||
let uom: string | undefined;
|
||||
|
||||
if (!entityIds) {
|
||||
return undefined;
|
||||
// Track devices that have sensor entities contributing values
|
||||
// to avoid duplicate readings from climate/humidifier attributes
|
||||
const devicesWithSensorValues = new Set<string>();
|
||||
|
||||
for (const entityId of sensorEntityIds) {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (
|
||||
stateObj &&
|
||||
!isUnavailableState(stateObj.state) &&
|
||||
isNumericState(stateObj) &&
|
||||
!isNaN(Number(stateObj.state))
|
||||
) {
|
||||
if (!uom) {
|
||||
uom = stateObj.attributes.unit_of_measurement;
|
||||
}
|
||||
if (stateObj.attributes.unit_of_measurement === uom) {
|
||||
values.push(Number(stateObj.state));
|
||||
// Track the device this sensor belongs to
|
||||
const entityEntry = this.hass.entities[entityId];
|
||||
if (entityEntry?.device_id) {
|
||||
devicesWithSensorValues.add(entityEntry.device_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all entities have state
|
||||
const entities = entityIds
|
||||
.map((entityId) => this.hass.states[entityId])
|
||||
.filter(Boolean);
|
||||
// Collect values from additional attribute sources
|
||||
const attrSources = SENSOR_ATTRIBUTE_SOURCES[sensorClass];
|
||||
if (attrSources) {
|
||||
const domains = [...new Set(attrSources.map((s) => s.domain))];
|
||||
const attrEntityIds = this._domainEntityIds(
|
||||
this.hass.entities,
|
||||
area.area_id,
|
||||
domains,
|
||||
excludeEntities
|
||||
);
|
||||
|
||||
if (entities.length === 0) {
|
||||
return undefined;
|
||||
for (const entityId of attrEntityIds) {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) continue;
|
||||
|
||||
// Skip if this entity's device already has a sensor contributing values
|
||||
const entityEntry = this.hass.entities[entityId];
|
||||
if (
|
||||
entityEntry?.device_id &&
|
||||
devicesWithSensorValues.has(entityEntry.device_id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const domain = entityId.split(".")[0];
|
||||
const source = attrSources.find((s) => s.domain === domain);
|
||||
if (!source) continue;
|
||||
|
||||
const attrValue = stateObj.attributes[source.attribute];
|
||||
if (attrValue == null || isNaN(Number(attrValue))) continue;
|
||||
|
||||
if (!uom) {
|
||||
// Determine unit from attribute
|
||||
uom = this._getAttributeUnit(sensorClass, domain);
|
||||
}
|
||||
values.push(Number(attrValue));
|
||||
}
|
||||
}
|
||||
|
||||
// If only one entity, return its formatted state
|
||||
if (entities.length === 1) {
|
||||
const stateObj = entities[0];
|
||||
return isUnavailableState(stateObj.state)
|
||||
? ""
|
||||
: this.hass.formatEntityState(stateObj);
|
||||
}
|
||||
|
||||
// Use the first entity's unit_of_measurement for formatting
|
||||
const uom = entities.find(
|
||||
(entity) => entity.attributes.unit_of_measurement
|
||||
)?.attributes.unit_of_measurement;
|
||||
|
||||
// Ensure all entities have the same unit_of_measurement
|
||||
const validEntities = entities.filter(
|
||||
(entity) =>
|
||||
entity.attributes.unit_of_measurement === uom &&
|
||||
isNumericState(entity) &&
|
||||
!isNaN(Number(entity.state))
|
||||
);
|
||||
|
||||
if (validEntities.length === 0) {
|
||||
if (values.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = SUM_DEVICE_CLASSES.includes(sensorClass)
|
||||
? this._computeSumState(validEntities)
|
||||
: this._computeMedianState(validEntities);
|
||||
? values.reduce((acc, v) => acc + v, 0)
|
||||
: this._computeMedianValue(values);
|
||||
|
||||
const formattedAverage = formatNumber(value, this.hass!.locale, {
|
||||
const formattedValue = formatNumber(value, this.hass.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
const formattedUnit = uom
|
||||
? `${blankBeforeUnit(uom, this.hass!.locale)}${uom}`
|
||||
? `${blankBeforeUnit(uom, this.hass.locale)}${uom}`
|
||||
: "";
|
||||
|
||||
return `${formattedAverage}${formattedUnit}`;
|
||||
return `${formattedValue}${formattedUnit}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
@@ -418,20 +482,25 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
return sensorStates;
|
||||
}
|
||||
|
||||
private _computeSumState(entities: HassEntity[]): number {
|
||||
return entities.reduce((acc, entity) => acc + Number(entity.state), 0);
|
||||
private _getAttributeUnit(sensorClass: string, domain: string): string {
|
||||
// Return the expected unit for attributes from specific domains
|
||||
if (sensorClass === "temperature" && domain === "climate") {
|
||||
return this.hass.config.unit_system.temperature;
|
||||
}
|
||||
if (sensorClass === "humidity") {
|
||||
return "%";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private _computeMedianState(entities: HassEntity[]): number {
|
||||
const sortedStates = entities
|
||||
.map((entity) => Number(entity.state))
|
||||
.sort((a, b) => a - b);
|
||||
if (sortedStates.length % 2 === 0) {
|
||||
const medianIndex = sortedStates.length / 2;
|
||||
return (sortedStates[medianIndex] + sortedStates[medianIndex - 1]) / 2;
|
||||
private _computeMedianValue(values: number[]): number {
|
||||
const sortedValues = [...values].sort((a, b) => a - b);
|
||||
if (sortedValues.length % 2 === 0) {
|
||||
const medianIndex = sortedValues.length / 2;
|
||||
return (sortedValues[medianIndex] + sortedValues[medianIndex - 1]) / 2;
|
||||
}
|
||||
const medianIndex = Math.floor(sortedStates.length / 2);
|
||||
return sortedStates[medianIndex];
|
||||
const medianIndex = Math.floor(sortedValues.length / 2);
|
||||
return sortedValues[medianIndex];
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne((config: AreaCardConfig) => {
|
||||
|
||||
@@ -94,10 +94,12 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
|
||||
(changedProps.has("_config") && this._config?.entities)
|
||||
) {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
this._calendars = this._config!.entities.map((entity, idx) => ({
|
||||
entity_id: entity,
|
||||
backgroundColor: getColorByIndex(idx, computedStyles),
|
||||
}));
|
||||
if (this._config?.entities) {
|
||||
this._calendars = this._config.entities.map((entity, idx) => ({
|
||||
entity_id: entity,
|
||||
backgroundColor: getColorByIndex(idx, computedStyles),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +73,18 @@ export class HuiCard extends ConditionalListenerMixin<LovelaceCardConfig>(
|
||||
};
|
||||
|
||||
// If the element has fixed rows or columns, we use the values from the element
|
||||
// unless the user has already configured their own
|
||||
if (elementOptions.fixed_rows) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
if (configOptions.rows === undefined) {
|
||||
mergedConfig.rows = elementOptions.rows;
|
||||
}
|
||||
delete mergedConfig.min_rows;
|
||||
delete mergedConfig.max_rows;
|
||||
}
|
||||
if (elementOptions.fixed_columns) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
if (configOptions.columns === undefined) {
|
||||
mergedConfig.columns = elementOptions.columns;
|
||||
}
|
||||
delete mergedConfig.min_columns;
|
||||
delete mergedConfig.max_columns;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,116 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCard } from "../types";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { EmptyStateCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-empty-state-card")
|
||||
export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-empty-state-card-editor");
|
||||
return document.createElement("hui-empty-state-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): EmptyStateCardConfig {
|
||||
return {
|
||||
type: "empty-state",
|
||||
title: "Welcome Home",
|
||||
content: "This is an empty state card.",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EmptyStateCardConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
public setConfig(_config: EmptyStateCardConfig): void {}
|
||||
public setConfig(config: EmptyStateCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.empty_state.title"
|
||||
)}
|
||||
class=${classMap({
|
||||
"content-only": this._config.content_only ?? false,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.empty_state.no_devices"
|
||||
)}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button appearance="plain" href="/config/integrations/dashboard">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.empty_state.go_to_integrations_page"
|
||||
)}
|
||||
</ha-button>
|
||||
<div class="container">
|
||||
${this._config.icon
|
||||
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${this._config.title ? html`<h1>${this._config.title}</h1>` : nothing}
|
||||
${this._config.content
|
||||
? html`<p>${this._config.content}</p>`
|
||||
: nothing}
|
||||
${this._config.tap_action && this._config.action_button_text
|
||||
? html`
|
||||
<ha-button @click=${this._handleAction}>
|
||||
${this._config.action_button_text}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(): void {
|
||||
if (this._config?.tap_action && this.hass) {
|
||||
handleAction(this, this.hass, this._config, "tap");
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
margin-top: -1em;
|
||||
padding: 16px;
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-button {
|
||||
margin-left: -8px;
|
||||
margin-inline-start: -8px;
|
||||
margin-inline-end: initial;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
padding: var(--ha-space-8) var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
gap: var(--ha-space-4);
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-icon {
|
||||
--mdc-icon-size: var(--ha-space-12);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: 500;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.content-only {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ import { findEntities } from "../common/find-entities";
|
||||
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
|
||||
import type { PictureElementsCardConfig } from "./types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "./types";
|
||||
import type { PersonEntity } from "../../../data/person";
|
||||
|
||||
@customElement("hui-picture-elements-card")
|
||||
@@ -166,6 +169,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
.aspectRatio=${this._config.aspect_ratio}
|
||||
.darkModeFilter=${this._config.dark_mode_filter}
|
||||
.darkModeImage=${darkModeImage}
|
||||
@click=${this._handleImageClick}
|
||||
></hui-image>
|
||||
${this._elements}
|
||||
</div>
|
||||
@@ -221,6 +225,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
|
||||
curCardEl === elToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
|
||||
private _handleImageClick(ev: MouseEvent): void {
|
||||
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 100;
|
||||
const y = ((ev.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
// only the edited card has this callback
|
||||
this._config[PREVIEW_CLICK_CALLBACK](x, y);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -8,7 +8,10 @@ import { createSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import {
|
||||
getEnergyDataCollection,
|
||||
getSuggestedPeriod,
|
||||
} from "../../../data/energy";
|
||||
import type {
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
@@ -26,10 +29,7 @@ import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import type { EntityConfig } from "../entity-rows/types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import {
|
||||
getSuggestedMax,
|
||||
getSuggestedPeriod,
|
||||
} from "./energy/common/energy-chart-options";
|
||||
import { getSuggestedMax } from "./energy/common/energy-chart-options";
|
||||
import type { StatisticsGraphCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_DAYS_TO_SHOW = 30;
|
||||
@@ -268,9 +268,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
return (
|
||||
this._config?.period ??
|
||||
(this._energyStart && this._energyEnd
|
||||
? getSuggestedPeriod(
|
||||
differenceInDays(this._energyEnd, this._energyStart)
|
||||
)
|
||||
? getSuggestedPeriod(this._energyStart, this._energyEnd)
|
||||
: undefined)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,8 +58,12 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
|
||||
}
|
||||
|
||||
export interface EmptyStateCardConfig extends LovelaceCardConfig {
|
||||
content: string;
|
||||
content_only?: boolean;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
action_button_text?: string;
|
||||
tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface EntityCardConfig extends LovelaceCardConfig {
|
||||
@@ -483,6 +487,10 @@ export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
alt_text?: string;
|
||||
}
|
||||
|
||||
// Symbol for preview click callback - preserved through spreads, not serialized
|
||||
// This allows the editor to attach a callback that only exists on the edited card's config
|
||||
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
|
||||
|
||||
export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
image?: string | MediaSelectorValue;
|
||||
@@ -497,6 +505,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
dark_mode_image?: string | MediaSelectorValue;
|
||||
dark_mode_filter?: string;
|
||||
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
|
||||
}
|
||||
|
||||
export interface PictureEntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -21,5 +21,8 @@ export const confirmAction = async (
|
||||
hass.localize("ui.panel.lovelace.cards.actions.action_confirmation", {
|
||||
action,
|
||||
}),
|
||||
title: config.title,
|
||||
dismissText: config.dismiss_text,
|
||||
confirmText: config.confirm_text,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -368,6 +368,7 @@ export const generateViewConfig = (
|
||||
path: string,
|
||||
title: string | undefined,
|
||||
icon: string | undefined,
|
||||
show_icon_and_title: boolean | undefined,
|
||||
entities: HassEntities
|
||||
): LovelaceViewConfig => {
|
||||
const ungroupedEntitites: Record<string, string[]> = {};
|
||||
@@ -497,6 +498,9 @@ export const generateViewConfig = (
|
||||
if (icon) {
|
||||
view.icon = icon;
|
||||
}
|
||||
if (show_icon_and_title) {
|
||||
view.show_icon_and_title = show_icon_and_title;
|
||||
}
|
||||
|
||||
return view;
|
||||
};
|
||||
@@ -517,6 +521,7 @@ export const generateDefaultViewConfig = (
|
||||
const path = "default_view";
|
||||
const title = "Home";
|
||||
const icon = undefined;
|
||||
const show_icon_and_title = undefined;
|
||||
|
||||
// In the case of a default view, we want to use the group order attribute
|
||||
const groupOrders = {};
|
||||
@@ -566,6 +571,7 @@ export const generateDefaultViewConfig = (
|
||||
path,
|
||||
title,
|
||||
icon,
|
||||
show_icon_and_title,
|
||||
splittedByGroups.ungrouped
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ const calcPoints = (
|
||||
height: number,
|
||||
limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number }
|
||||
) => {
|
||||
// handling empty history (for example unavailable for long time)
|
||||
if (history.length === 0) {
|
||||
return { points: [], yAxisOrigin: height };
|
||||
}
|
||||
|
||||
let yAxisOrigin = height;
|
||||
let minY = limits?.minY ?? history[0][1];
|
||||
let maxY = limits?.maxY ?? history[0][1];
|
||||
|
||||
@@ -89,6 +89,9 @@ export const handleAction = async (
|
||||
) ||
|
||||
actionConfig.action,
|
||||
}),
|
||||
title: actionConfig.confirmation.title,
|
||||
dismissText: actionConfig.confirmation.dismiss_text,
|
||||
confirmText: actionConfig.confirmation.confirm_text,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EmptyStateCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
content_only: optional(boolean()),
|
||||
icon: optional(string()),
|
||||
title: optional(string()),
|
||||
content: optional(string()),
|
||||
action_button_text: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("hui-empty-state-card-editor")
|
||||
export class HuiEmptyStateCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EmptyStateCardConfig;
|
||||
|
||||
public setConfig(config: EmptyStateCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: (
|
||||
[
|
||||
{ value: "card", image: "card" },
|
||||
{ value: "content-only", image: "text_only" },
|
||||
] as const
|
||||
).map((style) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.empty_state.style_options.${style.value}`
|
||||
),
|
||||
image: {
|
||||
src: `/static/images/form/markdown_${style.image}.svg`,
|
||||
src_dark: `/static/images/form/markdown_${style.image}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
value: style.value,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "icon", selector: { icon: {} } },
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "content", selector: { text: { multiline: true } } },
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{ name: "action_button_text", selector: { text: {} } },
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
style: this._config.content_only ? "content-only" : "card",
|
||||
};
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
const config = { ...ev.detail.value };
|
||||
|
||||
if (config.style === "content-only") {
|
||||
config.content_only = true;
|
||||
} else {
|
||||
delete config.content_only;
|
||||
}
|
||||
delete config.style;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "style":
|
||||
case "content":
|
||||
case "action_button_text":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-empty-state-card-editor": HuiEmptyStateCardEditor;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,16 @@ import {
|
||||
} from "superstruct";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { PictureElementsCardConfig } from "../../cards/types";
|
||||
import {
|
||||
PREVIEW_CLICK_CALLBACK,
|
||||
type PictureElementsCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
@@ -28,7 +32,6 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "../hui-picture-elements-card-row-editor";
|
||||
import type { LovelaceElementConfig } from "../../elements/types";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const genericElementConfigStruct = type({
|
||||
@@ -66,6 +69,44 @@ export class HuiPictureElementsCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onPreviewClick = (x: number, y: number): void => {
|
||||
if (this._subElementEditorConfig?.type === "element") {
|
||||
this._handlePositionClick(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
private _handlePositionClick(x: number, y: number): void {
|
||||
if (
|
||||
!this._subElementEditorConfig?.elementConfig ||
|
||||
this._subElementEditorConfig.type !== "element" ||
|
||||
this._subElementEditorConfig.elementConfig.type === "conditional"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementConfig = this._subElementEditorConfig
|
||||
.elementConfig as LovelaceElementConfig;
|
||||
const currentPosition = (elementConfig.style as Record<string, string>)
|
||||
?.position;
|
||||
if (currentPosition && currentPosition !== "absolute") {
|
||||
return;
|
||||
}
|
||||
|
||||
const newElement = {
|
||||
...elementConfig,
|
||||
style: {
|
||||
...((elementConfig.style as Record<string, string>) || {}),
|
||||
left: `${Math.round(x)}%`,
|
||||
top: `${Math.round(y)}%`,
|
||||
},
|
||||
};
|
||||
|
||||
const updateEvent = new CustomEvent("config-changed", {
|
||||
detail: { config: newElement },
|
||||
});
|
||||
this._handleSubElementChanged(updateEvent);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
return html`
|
||||
${this._subElementEditorConfig.type === "element" &&
|
||||
this._subElementEditorConfig.elementConfig?.type !== "conditional"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
@@ -181,6 +232,7 @@ export class HuiPictureElementsCardEditor
|
||||
return;
|
||||
}
|
||||
|
||||
// no need to attach the preview click callback here, no element is being edited
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
@@ -191,7 +243,8 @@ export class HuiPictureElementsCardEditor
|
||||
const config = {
|
||||
...this._config,
|
||||
elements: ev.detail.elements as LovelaceElementConfig[],
|
||||
} as LovelaceCardConfig;
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
} as PictureElementsCardConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
|
||||
@@ -232,7 +285,12 @@ export class HuiPictureElementsCardEditor
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
fireEvent(this, "config-changed", {
|
||||
config: {
|
||||
...this._config,
|
||||
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
|
||||
|
||||
@@ -10,7 +10,9 @@ export const getElementStubConfig = async (
|
||||
): Promise<LovelaceElementConfig> => {
|
||||
let elementConfig: LovelaceElementConfig = { type };
|
||||
|
||||
if (type !== "conditional") {
|
||||
if (type === "conditional") {
|
||||
elementConfig = { type, conditions: [], elements: [] };
|
||||
} else {
|
||||
elementConfig.style = { left: "50%", top: "50%" };
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,11 @@ export abstract class HuiElementEditor<
|
||||
}
|
||||
|
||||
public set value(config: T | undefined) {
|
||||
if (this._config && deepEqual(config, this._config)) {
|
||||
// Compare symbols to detect callback changes (e.g., preview click handlers)
|
||||
if (
|
||||
this._config &&
|
||||
deepEqual(config, this._config, { compareSymbols: true })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
|
||||
@@ -73,6 +73,12 @@ export class HuiViewEditor extends LitElement {
|
||||
icon: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "show_icon_and_title",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{ name: "path", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{
|
||||
@@ -207,6 +213,7 @@ export class HuiViewEditor extends LitElement {
|
||||
case "path":
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url");
|
||||
case "type":
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "max_columns":
|
||||
case "dense_section_placement":
|
||||
@@ -227,6 +234,7 @@ export class HuiViewEditor extends LitElement {
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "path":
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
|
||||
@@ -112,7 +112,20 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
forwardHaptic(this, "light");
|
||||
|
||||
setSelectOption(this.hass!, stateObj.entity_id, option);
|
||||
setSelectOption(this.hass!, stateObj.entity_id, option)
|
||||
.catch((_err) => {
|
||||
// silently swallow exception
|
||||
})
|
||||
.finally(() =>
|
||||
setTimeout(() => {
|
||||
const newStateObj = this.hass!.states[this._config!.entity];
|
||||
if (newStateObj === stateObj) {
|
||||
const select = this.shadowRoot?.querySelector("ha-select");
|
||||
const index = select?.options.indexOf(stateObj.state) ?? -1;
|
||||
select?.select(index);
|
||||
}
|
||||
}, 2000)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -496,6 +496,10 @@ class HUIRoot extends LitElement {
|
||||
|
||||
const tabs = html`<ha-tab-group @wa-tab-show=${this._handleViewSelected}>
|
||||
${views.map((view, index) => {
|
||||
const icon_and_title =
|
||||
view.show_icon_and_title && view.icon && view.title;
|
||||
const icon_only = view.icon && !icon_and_title;
|
||||
const title_only = !icon_only && !icon_and_title;
|
||||
const hidden =
|
||||
!this._editMode && (view.subview || _isTabHiddenForUser(view));
|
||||
return html`
|
||||
@@ -506,7 +510,8 @@ class HUIRoot extends LitElement {
|
||||
.disabled=${hidden}
|
||||
aria-label=${ifDefined(view.title)}
|
||||
class=${classMap({
|
||||
icon: Boolean(view.icon),
|
||||
"icon-only": Boolean(icon_only),
|
||||
"icon-and-title": Boolean(icon_and_title),
|
||||
"hide-tab": Boolean(hidden),
|
||||
})}
|
||||
>
|
||||
@@ -523,18 +528,20 @@ class HUIRoot extends LitElement {
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: nothing}
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
class=${classMap({
|
||||
"child-view-icon": Boolean(view.subview),
|
||||
})}
|
||||
title=${ifDefined(view.title)}
|
||||
.icon=${view.icon}
|
||||
></ha-icon>
|
||||
`
|
||||
: view.title ||
|
||||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")}
|
||||
${icon_only || icon_and_title
|
||||
? html`<ha-icon
|
||||
class=${classMap({
|
||||
"child-view-icon": Boolean(view.subview),
|
||||
})}
|
||||
title=${ifDefined(view.title)}
|
||||
.icon=${view.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${icon_and_title ? view.title : nothing}
|
||||
${title_only
|
||||
? view.title ||
|
||||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")
|
||||
: nothing}
|
||||
${this._editMode
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@@ -1489,24 +1496,27 @@ class HUIRoot extends LitElement {
|
||||
ha-tab-group-tab {
|
||||
--ha-tab-group-tab-height: var(--header-height, 56px);
|
||||
}
|
||||
.tab-bar ha-tab-group-tab {
|
||||
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
|
||||
}
|
||||
ha-tab-group-tab[aria-selected="true"] .edit-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding-inline-start: var(--ha-tab-padding-start, var(--wa-space-l));
|
||||
padding-inline-end: var(--ha-tab-padding-end, var(--wa-space-l));
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2);
|
||||
}
|
||||
ha-tab-group-tab.icon::part(base) {
|
||||
ha-tab-group-tab.icon-only::part(base),
|
||||
ha-tab-group-tab.icon-and-title::part(base) {
|
||||
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2 - 2px);
|
||||
padding-bottom: calc(
|
||||
(var(--ha-tab-group-tab-height) - 20px) / 2 - 4px
|
||||
);
|
||||
}
|
||||
.tab-bar ha-tab-group-tab {
|
||||
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
|
||||
ha-tab-group-tab.icon-and-title ha-icon {
|
||||
margin-inline-end: var(--ha-space-2);
|
||||
}
|
||||
.edit-mode ha-tab-group-tab[aria-selected="true"]::part(base) {
|
||||
padding: 0;
|
||||
|
||||
@@ -11,7 +11,10 @@ import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import type {
|
||||
EmptyStateCardConfig,
|
||||
HeadingCardConfig,
|
||||
} from "../../cards/types";
|
||||
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
getSummaryLabel,
|
||||
@@ -354,6 +357,26 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
// No sections, show empty state
|
||||
if (sections.length === 0) {
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:sofa-outline",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.empty_state_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.empty_state_content"
|
||||
),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
||||
const maxColumns = clamp(sections.length, 2, 3);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AreasDisplayValue } from "../../../../components/ha-areas-display-
|
||||
import { getEnergyPreferences } from "../../../../data/energy";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EmptyStateCardConfig } from "../../cards/types";
|
||||
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
|
||||
|
||||
export interface OriginalStatesViewStrategyConfig {
|
||||
@@ -64,9 +65,33 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
|
||||
|
||||
// User has no entities
|
||||
if (view.cards!.length === 0) {
|
||||
view.cards!.push({
|
||||
type: "empty-state",
|
||||
});
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:home-assistant",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_content"
|
||||
),
|
||||
...(hass.user?.is_admin
|
||||
? {
|
||||
action_button_text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_action"
|
||||
),
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/integrations/dashboard",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return view;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiAlphaABoxOutline,
|
||||
mdiArrowLeft,
|
||||
mdiDotsVertical,
|
||||
mdiGrid,
|
||||
mdiListBoxOutline,
|
||||
@@ -97,7 +96,6 @@ class PanelMediaBrowser extends LitElement {
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
slot="navigationIcon"
|
||||
.path=${mdiArrowLeft}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user