mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-16 04:07:38 +00:00
Compare commits
39 Commits
ai-task-sh
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905435db3e | ||
|
|
ea73fd3f01 | ||
|
|
e519a0203e | ||
|
|
d98ee7e0b5 | ||
|
|
6fc8c17909 | ||
|
|
201169c3d8 | ||
|
|
303538ac21 | ||
|
|
3c5a6193d0 | ||
|
|
5ee4bd63f8 | ||
|
|
b193929bd9 | ||
|
|
3bee5c8cd4 | ||
|
|
976c74b8da | ||
|
|
3a4a13db21 | ||
|
|
a2f033dd88 | ||
|
|
a44b94c8df | ||
|
|
8796830ff9 | ||
|
|
bdff13d5e1 | ||
|
|
4346484afc | ||
|
|
533694391e | ||
|
|
3adba7aa1f | ||
|
|
b60552c025 | ||
|
|
3011d56101 | ||
|
|
c903c0d734 | ||
|
|
14be390994 | ||
|
|
d48019a48e | ||
|
|
7b5cbb76ef | ||
|
|
c75fab025f | ||
|
|
c007206fa0 | ||
|
|
ab5b5a4276 | ||
|
|
9eb40f8470 | ||
|
|
bc827d9bf1 | ||
|
|
24f5d58691 | ||
|
|
13505a9104 | ||
|
|
c1d135aa16 | ||
|
|
16d13c3202 | ||
|
|
46b3c34ba1 | ||
|
|
57a81b9de4 | ||
|
|
69f4f1dbed | ||
|
|
355a1aff3f |
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -22,13 +22,11 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
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,9 +213,7 @@ 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$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -187,11 +187,5 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
50
package.json
50
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.9",
|
||||
"@codemirror/view": "6.39.7",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@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",
|
||||
"@formatjs/intl-datetimeformat": "7.1.0",
|
||||
"@formatjs/intl-displaynames": "7.1.0",
|
||||
"@formatjs/intl-durationformat": "0.9.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.0",
|
||||
"@formatjs/intl-listformat": "8.1.0",
|
||||
"@formatjs/intl-locale": "5.1.0",
|
||||
"@formatjs/intl-numberformat": "9.1.0",
|
||||
"@formatjs/intl-pluralrules": "6.1.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.0",
|
||||
"@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.9",
|
||||
"intl-messageformat": "11.0.7",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -150,14 +150,14 @@
|
||||
"@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.8",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||
"@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.1",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@rspack/core": "1.6.8",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -176,7 +176,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -199,7 +199,7 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.4.0",
|
||||
"jsdom": "27.3.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -215,27 +215,27 @@
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.52.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17",
|
||||
"typescript-eslint": "8.50.1",
|
||||
"vite-tsconfig-paths": "6.0.3",
|
||||
"vitest": "4.0.16",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
},
|
||||
"resolutions": {
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.0.0",
|
||||
"globals": "16.5.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.0"
|
||||
"node": "24.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251229.0"
|
||||
version = "20260107.1"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -38,11 +38,13 @@ 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}
|
||||
@@ -53,17 +55,18 @@ 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>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
|
||||
@@ -93,8 +93,8 @@ export const calcDateRange = (
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 12),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 0),
|
||||
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
|
||||
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* 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,16 +1,6 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -28,7 +18,7 @@ export const deepEqual = (
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +35,7 @@ export const deepEqual = (
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -103,28 +93,11 @@ export const deepEqual = (
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
ECElementEvent,
|
||||
} from "echarts/types/src/util/types";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -25,7 +21,6 @@ export interface Node {
|
||||
label?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
entityId?: string;
|
||||
}
|
||||
export interface Link {
|
||||
source: string;
|
||||
@@ -88,7 +83,6 @@ export class HaSankeyChart extends LitElement {
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
@@ -109,22 +103,6 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
if (detail.dataType !== "node") {
|
||||
return;
|
||||
}
|
||||
const nodeId = (detail.data as Record<string, any>)?.id;
|
||||
if (!nodeId) {
|
||||
return;
|
||||
}
|
||||
const node = this.data.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.entityId) {
|
||||
fireEvent(this, "node-click", { node });
|
||||
}
|
||||
};
|
||||
|
||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
|
||||
@@ -316,7 +294,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sankey-chart": HaSankeyChart;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"node-click": { node: Node };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ 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);
|
||||
@@ -185,7 +184,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
|
||||
@@ -1364,9 +1364,6 @@ 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,9 +1,8 @@
|
||||
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 { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
@@ -12,12 +11,11 @@ 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,
|
||||
@@ -30,7 +28,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.
|
||||
@@ -46,6 +44,12 @@ 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[],
|
||||
@@ -71,7 +75,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
if (!this.value || !this._automations) {
|
||||
if (!this.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
|
||||
if (idx === -1) {
|
||||
return this.value.alias || this.value.type || "unknown";
|
||||
return UNKNOWN_AUTOMATION_KEY;
|
||||
}
|
||||
|
||||
return `${this._automations[idx].device_id}_${idx}`;
|
||||
@@ -95,21 +99,37 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return nothing;
|
||||
}
|
||||
const value = this._value;
|
||||
|
||||
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>`;
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@@ -120,57 +140,6 @@ 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(
|
||||
@@ -192,14 +161,13 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
private _automationChanged(ev) {
|
||||
const value = ev.target.value;
|
||||
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
|
||||
return;
|
||||
}
|
||||
const [deviceId, idx] = value.split("_");
|
||||
const automation = this._automations![idx];
|
||||
const automation = this._automations[idx];
|
||||
if (automation.device_id !== deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
|
||||
@@ -96,9 +95,6 @@ export class HaStateContentPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-context" }) public allowContext =
|
||||
false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
@@ -110,12 +106,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _editIndex?: number;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
entityId?: string,
|
||||
stateObj?: HassEntity,
|
||||
allowName?: boolean,
|
||||
allowContext?: boolean
|
||||
) => {
|
||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
const items: PickerComboBoxItem[] = [
|
||||
{
|
||||
@@ -158,52 +149,6 @@ export class HaStateContentPicker extends LitElement {
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
},
|
||||
...(allowContext && stateObj
|
||||
? (() => {
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const contextItems: PickerComboBoxItem[] = [];
|
||||
if (context.device) {
|
||||
contextItems.push({
|
||||
id: "device_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.device_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.area) {
|
||||
contextItems.push({
|
||||
id: "area_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.area_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (context.floor) {
|
||||
contextItems.push({
|
||||
id: "floor_name",
|
||||
primary: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
sorting_label: this.hass.localize(
|
||||
"ui.components.state-content-picker.floor_name"
|
||||
),
|
||||
});
|
||||
}
|
||||
return contextItems;
|
||||
})()
|
||||
: []),
|
||||
...(domain
|
||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||
@@ -355,8 +300,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObjForItems,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
this.allowName
|
||||
);
|
||||
return items.find((item) => item.id === value)?.primary;
|
||||
}
|
||||
@@ -399,12 +343,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
@@ -428,12 +367,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(
|
||||
this.entityId,
|
||||
stateObj,
|
||||
this.allowName,
|
||||
this.allowContext
|
||||
);
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
|
||||
// If the search string does not match with the id of any of the items,
|
||||
// offer to add it as a custom attribute
|
||||
|
||||
@@ -143,19 +143,17 @@ export class HaEntityToggle extends LitElement {
|
||||
// Optimistic update.
|
||||
this._isOn = turnOn;
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -88,9 +88,6 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
@@ -121,33 +118,27 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
${!this.withoutHeader
|
||||
? html`<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>`
|
||||
: nothing}
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
@@ -165,7 +156,6 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
.withoutHeader=${this.withoutHeader}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
<ha-icon-button
|
||||
|
||||
@@ -6,10 +6,16 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { createAreaRegistryEntry } from "../data/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
} from "../data/device/device_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
@@ -24,6 +30,12 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "search_labels.areaName", weight: 10 },
|
||||
{ name: "search_labels.aliases", weight: 8 },
|
||||
{ name: "search_labels.floorName", weight: 6 },
|
||||
{ name: "search_labels.id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -90,8 +102,6 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
||||
@@ -127,13 +137,183 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: area.area_id,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreasMemoized(
|
||||
this._getAreas(
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -214,7 +394,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${areaComboBoxKeys}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
|
||||
@@ -174,14 +174,12 @@ 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,10 +51,7 @@ export class HaCard extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
:host
|
||||
::slotted(
|
||||
.card-content:not(:nth-child(1 of .card-content, .card-header))
|
||||
),
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
slot:not(:first-child)::slotted(.card-content) {
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
|
||||
@@ -255,7 +255,6 @@ 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,11 +1,9 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement } 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;
|
||||
@@ -15,8 +13,6 @@ 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;
|
||||
@@ -33,80 +29,41 @@ 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`
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.days))
|
||||
: Number(this.data.days)
|
||||
? Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -114,9 +71,7 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.hours))
|
||||
: Number(this.data.hours)
|
||||
? Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -124,9 +79,7 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _minutes() {
|
||||
return this.data?.minutes
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.minutes))
|
||||
: Number(this.data.minutes)
|
||||
? Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -134,9 +87,7 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _seconds() {
|
||||
return this.data?.seconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.seconds))
|
||||
: Number(this.data.seconds)
|
||||
? Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -144,9 +95,7 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
private get _milliseconds() {
|
||||
return this.data?.milliseconds
|
||||
? this.allowNegative
|
||||
? Math.abs(Number(this.data.milliseconds))
|
||||
: Number(this.data.milliseconds)
|
||||
? Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
@@ -164,14 +113,6 @@ 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;
|
||||
@@ -194,47 +135,12 @@ 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 {
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { updateAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../data/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { mdiLabel, mdiPlus } from "@mdi/js";
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
@@ -25,9 +25,11 @@ 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) {
|
||||
@@ -106,10 +108,52 @@ 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 = () =>
|
||||
this._getLabelsMemoized(
|
||||
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(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
@@ -122,6 +166,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeLabels
|
||||
);
|
||||
};
|
||||
|
||||
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
|
||||
if (!labels) {
|
||||
@@ -174,6 +219,8 @@ 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}
|
||||
@@ -190,6 +237,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -203,6 +251,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_LABELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -138,10 +138,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th {
|
||||
vertical-align: attr(valign, middle);
|
||||
vertical-align: attr(align, center);
|
||||
}
|
||||
td {
|
||||
vertical-align: attr(valign, middle);
|
||||
vertical-align: attr(align, left);
|
||||
}
|
||||
}
|
||||
table {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -26,8 +26,6 @@ import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -149,9 +147,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@@ -164,7 +160,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.virtualizerElement as HTMLElement | null;
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
@@ -211,17 +207,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
></ha-textfield>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
@@ -257,7 +244,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
@@ -290,18 +276,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
this.virtualizerElement &&
|
||||
this._virtualizerElement &&
|
||||
this.sectionTitleFunction &&
|
||||
this.sections?.length
|
||||
) {
|
||||
const firstItem = this.virtualizerElement.items[ev.first];
|
||||
const secondItem = this.virtualizerElement.items[ev.first + 1];
|
||||
const firstItem = this._virtualizerElement.items[ev.first];
|
||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem,
|
||||
secondItem: secondItem as PickerComboBoxItem,
|
||||
itemsCount: this.virtualizerElement.items.length,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -417,22 +403,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const newValue = value?.trim();
|
||||
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = "";
|
||||
this._searchFieldElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
@@ -514,8 +487,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,13 +511,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
if (!this.virtualizerElement) {
|
||||
if (!this._virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._searchFieldElement?.focus();
|
||||
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
const maxItems = items.length - 1;
|
||||
|
||||
@@ -578,14 +551,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectPreviousItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this.virtualizerElement) {
|
||||
if (!this._virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex > 0) {
|
||||
const nextIndex = this._selectedItemIndex - 1;
|
||||
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
@@ -607,13 +580,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectFirstItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -624,13 +597,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectLastItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = this.virtualizerElement.items.length - 1;
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -640,14 +613,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _scrollToSelectedItem = () => {
|
||||
this.virtualizerElement
|
||||
this._virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.virtualizerElement
|
||||
this._virtualizerElement
|
||||
?.querySelector(`#list-item-${this._selectedItemIndex}`)
|
||||
?.classList.add("selected");
|
||||
});
|
||||
@@ -655,20 +628,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (
|
||||
this.virtualizerElement?.items?.length !== undefined &&
|
||||
this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item
|
||||
this.virtualizerElement.items.filter((item) => typeof item !== "string")
|
||||
.length === 1
|
||||
) {
|
||||
(
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
}
|
||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||
|
||||
if (this._virtualizerElement?.items.length === 1) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: firstItem.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
@@ -678,16 +643,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
// if filter button is focused
|
||||
ev.preventDefault();
|
||||
|
||||
const item = this.virtualizerElement?.items[
|
||||
const item = this._virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
}
|
||||
};
|
||||
|
||||
private _resetSelectedItem() {
|
||||
this.virtualizerElement
|
||||
this._virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -697,11 +662,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this.virtualizerElement || this._search || !this.value) {
|
||||
if (!this._virtualizerElement || this._search || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this.virtualizerElement.items.findIndex(
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
@@ -726,10 +691,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host([clearable]) {
|
||||
--text-field-padding: 0 0 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
@@ -831,9 +792,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
.section-title,
|
||||
.title {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
@@ -862,7 +822,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
width: calc(100% - var(--ha-space-4));
|
||||
width: calc(100% - var(--ha-space-8));
|
||||
}
|
||||
|
||||
.section-title.show {
|
||||
@@ -886,8 +846,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picker-combo-box": HaPickerComboBox;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: entry.domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
@@ -248,6 +249,7 @@ export class HaRelatedItems extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: integration,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -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-2) var(--ha-space-3);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
@@ -12,10 +11,7 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selector!: DurationSelector;
|
||||
|
||||
@property({ attribute: false }) public value?:
|
||||
| HaDurationData
|
||||
| string
|
||||
| number;
|
||||
@property({ attribute: false }) public value?: HaDurationData;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -25,47 +21,16 @@ 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._data(this.value)}
|
||||
.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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export class HaMediaSelector extends LitElement {
|
||||
this._thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -37,7 +37,6 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.allowName=${this.selector.ui_state_content?.allow_name || false}
|
||||
.allowContext=${this.selector.ui_state_content?.allow_context || false}
|
||||
></ha-entity-state-content-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ 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,
|
||||
@@ -342,6 +344,17 @@ 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(() => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -50,6 +49,7 @@ 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.
|
||||
@@ -106,9 +106,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "without-header" })
|
||||
public withoutHeader = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@@ -117,8 +114,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
@@ -144,41 +139,33 @@ 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}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html` <slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span
|
||||
slot="title"
|
||||
class="title"
|
||||
id="ha-wa-dialog-title"
|
||||
>
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>`
|
||||
: nothing}
|
||||
<slot name="header">
|
||||
<ha-dialog-header
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
.showBorder=${this._bodyScrolled}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
@@ -221,11 +208,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
|
||||
if (ev.eventPhase === Event.AT_TARGET) {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
private _handleAfterHide = () => {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -238,23 +223,6 @@ 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,
|
||||
@@ -303,7 +271,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
color: var(--primary-text-color);
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
|
||||
@@ -24,7 +24,6 @@ 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";
|
||||
@@ -382,7 +381,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
|
||||
return `${path.name}<br>${formattedTime}`;
|
||||
}
|
||||
|
||||
private _drawPaths(): void {
|
||||
@@ -550,7 +549,7 @@ export class HaMap extends ReactiveElement {
|
||||
iconHTML = el.outerHTML;
|
||||
} else {
|
||||
const el = document.createElement("span");
|
||||
el.textContent = title;
|
||||
el.innerHTML = title;
|
||||
iconHTML = el.outerHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
} from "../../data/media-player";
|
||||
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-wa-dialog";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-list-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
@@ -44,8 +44,6 @@ 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 {
|
||||
@@ -56,11 +54,9 @@ 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;
|
||||
@@ -75,14 +71,21 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
flexContent
|
||||
.heading=${!this._currentItem
|
||||
? this.hass.localize(
|
||||
"ui.components.media-browser.media-player-browser"
|
||||
)
|
||||
: this._currentItem.title}
|
||||
@closed=${this.closeDialog}
|
||||
@opened=${this._dialogOpened}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@@ -149,7 +152,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
data-dialog="close"
|
||||
dialogAction="close"
|
||||
slot="actionItems"
|
||||
></ha-icon-button>
|
||||
</ha-dialog-header>
|
||||
@@ -169,7 +172,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
></ha-media-player-browse>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -221,7 +224,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
ha-dialog {
|
||||
--dialog-z-index: 9;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -236,9 +240,9 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-max-width: 800px;
|
||||
--ha-dialog-max-height: calc(
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 800px;
|
||||
--mdc-dialog-max-height: calc(
|
||||
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -793,6 +793,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
thumbnailUrl = brandsUrl({
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { mdiSpeaker } from "@mdi/js";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
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 {
|
||||
@@ -21,61 +20,15 @@ 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=${icon}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
|
||||
<div class="info">
|
||||
<div class="main-text">${primary}</div>
|
||||
<div class="secondary-text">${secondary}</div>
|
||||
<div class="main-text">${computeStateName(stateObj)}</div>
|
||||
<div class="secondary-text">
|
||||
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
|
||||
</div>
|
||||
</div>
|
||||
<ha-switch
|
||||
.disabled=${this.disabled}
|
||||
@@ -85,6 +38,16 @@ 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`
|
||||
|
||||
@@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../../common/entity/context/get_area_context";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
type DeviceRegistryEntry,
|
||||
} from "../device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
|
||||
|
||||
export const getAreas = (
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
idPrefix = ""
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, haFloors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: `${idPrefix}${area.area_id}`,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaId: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const areaComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.aliases",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaId",
|
||||
weight: 2,
|
||||
},
|
||||
];
|
||||
@@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../entity/entity_registry";
|
||||
import type { RegistryEntry } from "../registry";
|
||||
} from "./entity/entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "../ws-area_registry";
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry extends RegistryEntry {
|
||||
aliases: string[];
|
||||
@@ -449,9 +449,16 @@ const getEnergyData = async (
|
||||
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
|
||||
|
||||
const dayDifference = differenceInDays(end || new Date(), start);
|
||||
|
||||
const period = getSuggestedPeriod(start, end);
|
||||
const finePeriod = getSuggestedPeriod(start, end, true);
|
||||
const period =
|
||||
isFirstDayOfMonth(start) &&
|
||||
(!end || isLastDayOfMonth(end)) &&
|
||||
dayDifference > 35
|
||||
? "month"
|
||||
: dayDifference > 2
|
||||
? "day"
|
||||
: "hour";
|
||||
const finePeriod =
|
||||
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
|
||||
|
||||
const statsMetadata: Record<string, StatisticsMetaData> = {};
|
||||
const statsMetadataArray = allStatIDs.length
|
||||
@@ -582,7 +589,7 @@ const getEnergyData = async (
|
||||
consumptionStatIDs,
|
||||
co2SignalEntity,
|
||||
end,
|
||||
period
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
if (compare) {
|
||||
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
|
||||
@@ -591,7 +598,7 @@ const getEnergyData = async (
|
||||
consumptionStatIDs,
|
||||
co2SignalEntity,
|
||||
endCompare,
|
||||
period
|
||||
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1420,22 +1427,3 @@ 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,7 +69,6 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
|
||||
current_humidity: "%",
|
||||
min_humidity: "%",
|
||||
max_humidity: "%",
|
||||
target_humidity_step: "%",
|
||||
},
|
||||
light: {
|
||||
color_temp: "mired",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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" },
|
||||
@@ -54,13 +52,3 @@ 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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
@@ -16,7 +16,6 @@ export type HumidifierEntity = HassEntityBase & {
|
||||
mode?: string;
|
||||
action?: HumidifierAction;
|
||||
available_modes?: string[];
|
||||
target_humidity_step?: number;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@ export interface BaseActionConfig {
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
title?: string;
|
||||
confirm_text?: string;
|
||||
dismiss_text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ export interface LovelaceBaseViewConfig {
|
||||
title?: string;
|
||||
path?: string;
|
||||
icon?: string;
|
||||
show_icon_and_title?: boolean;
|
||||
theme?: string;
|
||||
panel?: boolean;
|
||||
background?: string | LovelaceViewBackgroundConfig;
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
@@ -74,40 +72,6 @@ export const getPanelTitleFromUrlPath = (
|
||||
return getPanelTitle(hass, panel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get subpage title for config panel routes.
|
||||
* Returns the specific subpage title (e.g., "Automations") if found,
|
||||
* or undefined to fall back to the panel title (e.g., "Settings").
|
||||
*
|
||||
* @param hass HomeAssistant instance
|
||||
* @param path Full route path (e.g., "/config/automation/dashboard")
|
||||
* @param configSections Config sections metadata for resolving subpage titles
|
||||
* @returns Localized subpage title, or undefined if not found
|
||||
*/
|
||||
export const getConfigSubpageTitle = (
|
||||
hass: HomeAssistant,
|
||||
path: string,
|
||||
configSections: Record<string, PageNavigation[]>
|
||||
): string | undefined => {
|
||||
// Search through all config section groups for a matching path
|
||||
for (const sectionGroup of Object.values(configSections)) {
|
||||
const pageNav = sectionGroup.find((nav) => path.startsWith(nav.path));
|
||||
if (pageNav) {
|
||||
if (pageNav.translationKey) {
|
||||
const localized = hass.localize(pageNav.translationKey as LocalizeKeys);
|
||||
if (localized) {
|
||||
return localized;
|
||||
}
|
||||
}
|
||||
|
||||
if (pageNav.name) {
|
||||
return pageNav.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
import {
|
||||
mdiKeyboard,
|
||||
mdiNavigationVariant,
|
||||
mdiPuzzle,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HassioAddonInfo } from "./hassio/addon";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
|
||||
export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
image?: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export interface BaseNavigationCommand {
|
||||
path: string;
|
||||
primary: string;
|
||||
icon_path?: string;
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
|
||||
action: string;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
export interface NavigationInfo extends PageNavigation {
|
||||
primary: string;
|
||||
}
|
||||
|
||||
const generateNavigationPanelCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
panels: HomeAssistant["panels"],
|
||||
addons?: HassioAddonInfo[]
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.entries(panels)
|
||||
.filter(
|
||||
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
)
|
||||
.map(([_panelKey, panel]) => {
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
const icon = getPanelIcon(panel) || "mdi:view-dashboard";
|
||||
|
||||
const primary = localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (addons) {
|
||||
const addon = addons.find(({ slug }) => slug === panel.url_path);
|
||||
if (addon) {
|
||||
image = addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primary,
|
||||
icon,
|
||||
image,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
|
||||
const getNavigationInfoFromConfig = (
|
||||
localize: HomeAssistant["localize"],
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined => {
|
||||
const path = page.path.substring(1);
|
||||
|
||||
let name = path.substring(path.indexOf("/") + 1);
|
||||
name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
|
||||
|
||||
const caption =
|
||||
(name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) ||
|
||||
// @ts-expect-error
|
||||
(page.translationKey && localize(page.translationKey));
|
||||
|
||||
if (caption) {
|
||||
return { ...page, primary: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
if (!hass.user?.is_admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
Object.values(configSections).forEach((sectionPages) => {
|
||||
sectionPages.forEach((page) => {
|
||||
if (!canShowPage(hass, page)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (items.some((e) => e.path === info.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const finalizeNavigationCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
items: BaseNavigationCommand[]
|
||||
): NavigationComboBoxItem[] =>
|
||||
items.map((item, index) => {
|
||||
const secondary = localize(
|
||||
"ui.dialogs.quick-bar.commands.types.navigation"
|
||||
);
|
||||
return {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
icon_path: item.iconPath || mdiNavigationVariant,
|
||||
secondary,
|
||||
sorting_label: `${item.primary}_${secondary}`,
|
||||
...item,
|
||||
};
|
||||
});
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant,
|
||||
addons?: HassioAddonInfo[]
|
||||
): NavigationComboBoxItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(
|
||||
hass.localize,
|
||||
hass.panels,
|
||||
addons
|
||||
);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
icon_path: mdiStorePlus,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_store"
|
||||
),
|
||||
});
|
||||
supervisorItems.push({
|
||||
path: "/hassio/dashboard",
|
||||
icon_path: mdiPuzzle,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
|
||||
),
|
||||
});
|
||||
if (addons) {
|
||||
for (const addon of addons.filter((a) => a.version)) {
|
||||
supervisorItems.push({
|
||||
path: `/hassio/addon/${addon.slug}`,
|
||||
image: addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_info",
|
||||
{ addon: addon.name }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const additionalItems = [
|
||||
{
|
||||
path: "",
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.shortcuts"
|
||||
),
|
||||
icon_path: mdiKeyboard,
|
||||
},
|
||||
];
|
||||
|
||||
return finalizeNavigationCommands(hass.localize, [
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...supervisorItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
};
|
||||
|
||||
const generateReloadCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primary:
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
|
||||
domain: domainToName(hass.localize, domain),
|
||||
}),
|
||||
domain,
|
||||
action: "reload",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`),
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
|
||||
domain: "frontend",
|
||||
action: "reload_themes",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_core_config",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_all"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_all",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
return commands.map((command, index) => ({
|
||||
...command,
|
||||
id: `command_${index}_${command.primary}`,
|
||||
sorting_label: `${command.primary}_${command.secondary}_${command.domain}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateServerControlCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
const serverActions = ["restart", "stop"] as const;
|
||||
|
||||
return serverActions.map((action, index) => {
|
||||
const primary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
{
|
||||
action: hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const secondary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.server_control"
|
||||
);
|
||||
|
||||
return {
|
||||
id: `server_control_${index}_${action}`,
|
||||
primary,
|
||||
domain: "homeassistant",
|
||||
icon_path: mdiServerNetwork,
|
||||
secondary,
|
||||
sorting_label: `${primary}_${secondary}_${action}`,
|
||||
action,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const generateActionCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => [
|
||||
...generateReloadCommands(hass),
|
||||
...generateServerControlCommands(hass),
|
||||
];
|
||||
|
||||
export const commandComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "domain",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
|
||||
export const navigateComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
@@ -221,7 +221,6 @@ export interface DurationSelector {
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
allow_negative?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -377,7 +376,7 @@ interface SelectBoxOptionImage {
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
value: any;
|
||||
label: string;
|
||||
description?: string;
|
||||
image?: string | SelectBoxOptionImage;
|
||||
@@ -501,7 +500,6 @@ export interface UiStateContentSelector {
|
||||
ui_state_content: {
|
||||
entity_id?: string;
|
||||
allow_name?: boolean;
|
||||
allow_context?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { FloorComboBoxItem } from "./area_floor_picker";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DevicePickerItem } from "./device/device_picker";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||
import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
theme: ThemeSettings;
|
||||
}
|
||||
}
|
||||
|
||||
export const subscribeThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
callback: (data: { value: ThemeSettings | null }) => void
|
||||
) => subscribeFrontendUserData(hass.connection, "theme", callback);
|
||||
|
||||
export const saveThemePreferences = (
|
||||
hass: HomeAssistant,
|
||||
data: ThemeSettings
|
||||
) => saveFrontendUserData(hass.connection, "theme", data);
|
||||
@@ -44,27 +44,14 @@ 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 =>
|
||||
updateAvailable(entity, showSkipped) &&
|
||||
(entity.state === BINARY_STATE_ON ||
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
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 &&
|
||||
@@ -121,17 +108,13 @@ export const filterUpdateEntities = (
|
||||
);
|
||||
});
|
||||
|
||||
export const filterUpdateEntitiesParameterized = (
|
||||
export const filterUpdateEntitiesWithInstall = (
|
||||
entities: HassEntities,
|
||||
showSkipped = false,
|
||||
showNotInstallable = false
|
||||
showSkipped = false
|
||||
) =>
|
||||
filterUpdateEntities(entities).filter((entity) => {
|
||||
if (showNotInstallable) {
|
||||
return updateCanNotInstall(entity, showSkipped);
|
||||
}
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
filterUpdateEntities(entities).filter((entity) =>
|
||||
updateCanInstall(entity, showSkipped)
|
||||
);
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
|
||||
const fetchAreaRegistry = (conn: Connection) =>
|
||||
conn.sendMessagePromise<AreaRegistryEntry[]>({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,14 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export type QuickBarSection =
|
||||
| "entity"
|
||||
| "device"
|
||||
| "area"
|
||||
| "navigate"
|
||||
| "command";
|
||||
export const enum QuickBarMode {
|
||||
Command = "command",
|
||||
Device = "device",
|
||||
Entity = "entity",
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
mode?: QuickBarMode;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
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";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
|
||||
@@ -37,10 +39,6 @@ const _SHORTCUTS: Section[] = [
|
||||
{
|
||||
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
|
||||
},
|
||||
{
|
||||
shortcut: [CTRL_CMD, "K"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search",
|
||||
},
|
||||
{
|
||||
shortcut: ["C"],
|
||||
descriptionTranslationKey:
|
||||
@@ -156,10 +154,6 @@ const _SHORTCUTS: Section[] = [
|
||||
shortcut: ["M"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
|
||||
},
|
||||
{
|
||||
shortcut: ["Shift", "/"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.other.show_shortcuts",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -168,22 +162,17 @@ const _SHORTCUTS: Section[] = [
|
||||
class DialogShortcuts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _open = false;
|
||||
@state() private _opened = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._open = true;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public async closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _renderShortcut(
|
||||
shortcutKeys: ShortcutString[],
|
||||
descriptionKey: LocalizeKeys
|
||||
@@ -195,7 +184,9 @@ 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
|
||||
@@ -210,11 +201,20 @@ class DialogShortcuts extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
defaultAction="ignore"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.shortcuts.title")
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
${_SHORTCUTS.map(
|
||||
@@ -237,7 +237,7 @@ class DialogShortcuts extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-alert slot="footer">
|
||||
<ha-alert>
|
||||
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
>${this.hass.localize(
|
||||
@@ -246,12 +246,25 @@ class DialogShortcuts extends LitElement {
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 15;
|
||||
}
|
||||
|
||||
h3:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -273,10 +286,6 @@ class DialogShortcuts extends LitElement {
|
||||
ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
ha-alert a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ window.loadES5Adapter = () => {
|
||||
};
|
||||
|
||||
let panelEl: HTMLElement | undefined;
|
||||
let initialized = false;
|
||||
|
||||
function setProperties(properties) {
|
||||
if (!panelEl) {
|
||||
@@ -129,23 +128,13 @@ function initialize(
|
||||
});
|
||||
}
|
||||
|
||||
function handleReady() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
window.parent.customPanel!.registerIframe(initialize, setProperties);
|
||||
}
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => window.parent.customPanel!.registerIframe(initialize, setProperties),
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
// Initial load
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", handleReady, { once: true });
|
||||
} else {
|
||||
handleReady();
|
||||
}
|
||||
|
||||
window.addEventListener("pageshow", handleReady);
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
initialized = false;
|
||||
window.addEventListener("unload", () => {
|
||||
// allow disconnected callback to fire
|
||||
while (document.body.lastChild) {
|
||||
document.body.removeChild(document.body.lastChild);
|
||||
|
||||
@@ -152,7 +152,7 @@ export const provideHass = (
|
||||
for (const ent of ensureArray(newEntities)) {
|
||||
hass().entities[ent.entityId] = {
|
||||
entity_id: ent.entityId,
|
||||
name: ent.attributes.friendly_name || null,
|
||||
name: ent.name,
|
||||
icon: ent.icon,
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
|
||||
@@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
} from "../../../data/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
|
||||
@@ -14,16 +14,16 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area/area_registry";
|
||||
} from "../../../data/area_registry";
|
||||
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type {
|
||||
FloorRegistryEntry,
|
||||
FloorRegistryEntryMutableParams,
|
||||
|
||||
@@ -23,11 +23,11 @@ import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
deleteAreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area/area_registry";
|
||||
} from "../../../data/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
|
||||
@@ -31,12 +31,12 @@ import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
createAreaRegistryEntry,
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area/area_registry";
|
||||
} from "../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import {
|
||||
createFloorRegistryEntry,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area/area_registry";
|
||||
} from "../../../data/area_registry";
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
|
||||
@@ -58,7 +58,6 @@ import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type {
|
||||
Action,
|
||||
DeviceAction,
|
||||
NonConditionAction,
|
||||
RepeatAction,
|
||||
ServiceAction,
|
||||
@@ -234,13 +233,6 @@ 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`
|
||||
@@ -262,7 +254,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${type === "service" && "target" in this.action
|
||||
? this._renderTargets((this.action as ServiceAction).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -863,7 +857,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -57,11 +57,11 @@ import {
|
||||
ACTION_COLLECTIONS,
|
||||
ACTION_ICONS,
|
||||
} from "../../../data/action";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
@@ -232,8 +232,6 @@ class DialogAddAutomationElement
|
||||
|
||||
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _closing = false;
|
||||
|
||||
// #endregion variables
|
||||
|
||||
// #region lifecycle
|
||||
@@ -349,8 +347,6 @@ class DialogAddAutomationElement
|
||||
}
|
||||
}
|
||||
|
||||
this._closing = true;
|
||||
|
||||
// if dialog is closed, but root level isn't active, clean up history state
|
||||
if (mainWindow.history.state?.dialogData) {
|
||||
this._open = false;
|
||||
@@ -364,7 +360,6 @@ class DialogAddAutomationElement
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._open = true;
|
||||
this._closing = false;
|
||||
this._params = undefined;
|
||||
this._selectedCollectionIndex = undefined;
|
||||
this._selectedGroup = undefined;
|
||||
@@ -436,24 +431,6 @@ class DialogAddAutomationElement
|
||||
|
||||
// #region render
|
||||
|
||||
private _getEmptyNote(automationElementType: string) {
|
||||
if (
|
||||
automationElementType !== "trigger" &&
|
||||
automationElementType !== "condition"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target_note`,
|
||||
{
|
||||
labs_link: html`<a href="/config/labs" @click=${this._close}
|
||||
>${this.hass.localize("ui.panel.config.labs.caption")}</a
|
||||
>`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -719,7 +696,6 @@ class DialogAddAutomationElement
|
||||
.emptyLabel=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
|
||||
)}
|
||||
.emptyNote=${this._getEmptyNote(automationElementType)}
|
||||
.tooltipDescription=${this._tab === "targets"}
|
||||
.target=${(this._tab === "targets" &&
|
||||
this._selectedTarget &&
|
||||
@@ -1715,9 +1691,9 @@ class DialogAddAutomationElement
|
||||
|
||||
// #region interaction
|
||||
|
||||
private _close = () => {
|
||||
private _close() {
|
||||
this._open = false;
|
||||
};
|
||||
}
|
||||
|
||||
private _back() {
|
||||
mainWindow.history.back();
|
||||
@@ -1899,10 +1875,7 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
private _handleClosed() {
|
||||
// if closing isn't already in progress, close the dialog
|
||||
if (!this._closing) {
|
||||
this.closeDialog();
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
// #region interaction
|
||||
@@ -2089,7 +2062,6 @@ class DialogAddAutomationElement
|
||||
|
||||
.content.column {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
|
||||
@@ -28,10 +28,6 @@ import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area/area_registry";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
type AreaFloorValue,
|
||||
@@ -39,6 +35,10 @@ import {
|
||||
type FloorNestedComboBoxItem,
|
||||
type UnassignedAreasFloorComboBoxItem,
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area_registry";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
@@ -1504,7 +1504,14 @@ 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);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px), all and (max-height: 500px) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -39,8 +39,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
|
||||
@property({ attribute: "empty-label" }) public emptyLabel!: string;
|
||||
|
||||
@property({ attribute: false }) public emptyNote?: string | TemplateResult;
|
||||
|
||||
@property({ attribute: false }) public target?: Target;
|
||||
|
||||
@property({ attribute: false }) public getLabel!: (
|
||||
@@ -81,9 +79,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
? html`${this.emptyLabel}
|
||||
${this.target
|
||||
? html`<div>${this._renderTarget(this.target)}</div>`
|
||||
: nothing}
|
||||
${this.emptyNote
|
||||
? html`<div class="empty-note">${this.emptyNote}</div>`
|
||||
: nothing}`
|
||||
: repeat(
|
||||
this.items,
|
||||
@@ -204,7 +199,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([scrollable]) .items {
|
||||
overflow: auto;
|
||||
@@ -219,24 +213,13 @@ export class HaAutomationAddItems extends LitElement {
|
||||
background-color: var(--ha-color-surface-default);
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
padding: var(--ha-space-4);
|
||||
padding: 0;
|
||||
margin: 0 var(--ha-space-4)
|
||||
max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
color: var(--ha-color-text-secondary);
|
||||
margin-top: var(--ha-space-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-note a {
|
||||
color: currentColor;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.items.error {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
color: var(--ha-color-on-danger-normal);
|
||||
@@ -302,8 +285,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -18,8 +19,13 @@ import "../../../../components/ha-textarea";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../category/ha-category-picker";
|
||||
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity/entity_registry";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label/label_registry";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -27,11 +33,6 @@ import type {
|
||||
EntityRegistryUpdate,
|
||||
SaveDialogParams,
|
||||
} from "./show-dialog-automation-save";
|
||||
import {
|
||||
generateMetadataSuggestionTask,
|
||||
processMetadataSuggestion,
|
||||
type MetadataSuggestionResult,
|
||||
} from "../../common/suggest-metadata-ai";
|
||||
|
||||
@customElement("ha-dialog-automation-save")
|
||||
class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
@@ -156,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"
|
||||
)}
|
||||
@@ -167,7 +168,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
autogrow
|
||||
.value=${this._newDescription}
|
||||
.helper=${supportsMarkdownHelper(this.hass.localize)}
|
||||
helperPersistent
|
||||
@input=${this._valueChanged}
|
||||
></ha-textarea>`
|
||||
: nothing}
|
||||
@@ -334,49 +334,180 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> =>
|
||||
generateMetadataSuggestionTask(this.hass, {
|
||||
domain: this._params.domain,
|
||||
config: this._params.config,
|
||||
includeDescription: true,
|
||||
});
|
||||
private _getSuggestData() {
|
||||
return Promise.all([
|
||||
subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
),
|
||||
fetchCategoryRegistry(this.hass.connection, "automation").then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
|
||||
const [labels, entities, categories] = await this._getSuggestData();
|
||||
const inspirations: string[] = [];
|
||||
|
||||
const domain = this._params.domain;
|
||||
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
const entityEntry = entities[entity.entity_id];
|
||||
if (
|
||||
computeStateDomain(entity) !== domain ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name ||
|
||||
!entityEntry
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
const category = categories[entityEntry.categories.automation];
|
||||
if (category) {
|
||||
inspiration += ` (category: ${category})`;
|
||||
}
|
||||
|
||||
if (entityEntry.labels.length) {
|
||||
inspiration += ` (labels: ${entityEntry.labels
|
||||
.map((label) => labels[label])
|
||||
.join(", ")})`;
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
}
|
||||
|
||||
const term = this._params.domain === "script" ? "script" : "automation";
|
||||
|
||||
return {
|
||||
type: "data",
|
||||
task: {
|
||||
task_name: `frontend__${term}__save`,
|
||||
instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant ${term}.
|
||||
|
||||
The name should be relevant to the ${term}'s purpose.
|
||||
${
|
||||
inspirations.length
|
||||
? `The name should be in same style and sentence capitalization as existing ${term}s.
|
||||
Suggest a category and labels if relevant to the ${term}'s purpose.
|
||||
Only suggest category and labels that are already used by existing ${term}s.`
|
||||
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
|
||||
}
|
||||
If the ${term} contains 5+ steps, include a short description.
|
||||
|
||||
For inspiration, here are existing ${term}s:
|
||||
${inspirations.join("\n")}
|
||||
|
||||
The ${term} configuration is as follows:
|
||||
|
||||
${dump(this._params.config)}
|
||||
`,
|
||||
structure: {
|
||||
name: {
|
||||
description: "The name of the automation",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
description: "A short description of the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
description: "Labels for the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {
|
||||
description: "The category of the automation",
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: Object.entries(categories).map(([id, name]) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private async _handleSuggestion(
|
||||
event: CustomEvent<GenDataTaskResult<MetadataSuggestionResult>>
|
||||
event: CustomEvent<
|
||||
GenDataTaskResult<{
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
}>
|
||||
>
|
||||
) {
|
||||
const result = event.detail;
|
||||
const processed = await processMetadataSuggestion(
|
||||
this.hass,
|
||||
this._params.domain,
|
||||
result
|
||||
);
|
||||
const [labels, _entities, categories] = await this._getSuggestData();
|
||||
|
||||
this._newName = processed.name;
|
||||
|
||||
if (processed.description) {
|
||||
this._newDescription = processed.description;
|
||||
this._newName = result.data.name;
|
||||
if (result.data.description) {
|
||||
this._newDescription = result.data.description;
|
||||
if (!this._visibleOptionals.includes("description")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "description"];
|
||||
}
|
||||
}
|
||||
|
||||
if (processed.categoryId) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
category: processed.categoryId,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("category")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "category"];
|
||||
if (result.data.category) {
|
||||
// We get back category name, convert it to ID
|
||||
const categoryId = Object.entries(categories).find(
|
||||
([, name]) => name === result.data.category
|
||||
)?.[0];
|
||||
if (categoryId) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
category: categoryId,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("category")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "category"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (processed.labelIds?.length) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
labels: processed.labelIds,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("labels")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
if (result.data.labels?.length) {
|
||||
// We get back label names, convert them to IDs
|
||||
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
||||
result.data.labels.map((name) => [name, undefined])
|
||||
);
|
||||
let toFind = result.data.labels.length;
|
||||
for (const [labelId, labelName] of Object.entries(labels)) {
|
||||
if (labelName in newLabels && newLabels[labelName] === undefined) {
|
||||
newLabels[labelName] = labelId;
|
||||
toFind--;
|
||||
if (toFind === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foundLabels = Object.values(newLabels).filter(
|
||||
(labelId) => labelId !== undefined
|
||||
);
|
||||
if (foundLabels.length) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
labels: foundLabels,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("labels")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,7 +570,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
ha-category-picker,
|
||||
ha-labels-picker,
|
||||
ha-area-picker,
|
||||
ha-chip-set:has(> ha-assist-chip) {
|
||||
ha-chip-set {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-alert {
|
||||
|
||||
@@ -76,7 +76,6 @@ 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;
|
||||
@@ -185,14 +184,6 @@ 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"
|
||||
@@ -203,7 +194,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${"target" in
|
||||
(this.conditionDescriptions[this.condition.condition] || {})
|
||||
? this._renderTargets((this.condition as PlatformCondition).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -839,7 +833,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -57,7 +57,6 @@ 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";
|
||||
@@ -67,7 +66,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import {
|
||||
deleteAutomation,
|
||||
@@ -116,8 +115,6 @@ 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;
|
||||
@@ -379,31 +376,6 @@ 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;
|
||||
}
|
||||
@@ -661,15 +633,6 @@ 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"}
|
||||
@@ -1040,7 +1003,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
? // @ts-ignore
|
||||
items.intersection(categoryItems)
|
||||
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
key === "ha-filter-labels" &&
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
@@ -1062,29 +1026,6 @@ 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;
|
||||
|
||||
@@ -350,7 +350,6 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -223,8 +223,6 @@ 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,7 +56,6 @@ 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";
|
||||
@@ -197,15 +196,6 @@ 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
|
||||
@@ -220,7 +210,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
></ha-trigger-icon>`}
|
||||
<h3 slot="header">
|
||||
${describeTrigger(this.trigger, this.hass, this._entityReg)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${type === "platform" &&
|
||||
"target" in
|
||||
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
|
||||
? this._renderTargets((this.trigger as PlatformTrigger).target)
|
||||
: nothing}
|
||||
</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
@@ -815,7 +809,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
ev.stopPropagation();
|
||||
const action = ev.detail?.item?.value;
|
||||
|
||||
if (!action) {
|
||||
|
||||
@@ -152,6 +152,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -66,6 +66,7 @@ class HaBackupAgentsPicker extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -226,6 +226,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
height="24"
|
||||
|
||||
@@ -207,6 +207,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized:
|
||||
this.hass.themes?.darkMode,
|
||||
})}
|
||||
|
||||
@@ -252,6 +252,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.src=${brandsUrl({
|
||||
domain: "cloud",
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
|
||||
@@ -206,8 +206,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
sortable: true,
|
||||
valueColumn: "usageCount",
|
||||
type: "numeric",
|
||||
minWidth: "90px",
|
||||
maxWidth: "90px",
|
||||
minWidth: "100px",
|
||||
maxWidth: "120px",
|
||||
template: (blueprint) => {
|
||||
const count = blueprint.usageCount ?? 0;
|
||||
return html`
|
||||
|
||||
@@ -4,8 +4,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -26,21 +28,14 @@ class DialogAssignCategory extends LitElement {
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: AssignCategoryDialogParams): void {
|
||||
this._params = params;
|
||||
this._scope = params.scope;
|
||||
this._category = params.entityReg.categories[params.scope];
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -52,46 +47,47 @@ class DialogAssignCategory extends LitElement {
|
||||
}
|
||||
const entry = this._params.entityReg.categories[this._params.scope];
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||
: this.hass.localize("ui.panel.config.category.assign.assign")}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||
: this.hass.localize("ui.panel.config.category.assign.assign")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-category-picker
|
||||
.hass=${this.hass}
|
||||
.scope=${this._scope}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.category-picker.category"
|
||||
)}
|
||||
.value=${this._category}
|
||||
@value-changed=${this._categoryChanged}
|
||||
autofocus
|
||||
></ha-category-picker>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-category-picker
|
||||
.hass=${this.hass}
|
||||
.scope=${this._scope}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.category-picker.category"
|
||||
)}
|
||||
.value=${this._category}
|
||||
@value-changed=${this._categoryChanged}
|
||||
></ha-category-picker>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!!this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-textfield";
|
||||
import type {
|
||||
@@ -30,14 +30,11 @@ class DialogCategoryDetail extends LitElement {
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
params: CategoryRegistryDetailDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._open = true;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || null;
|
||||
@@ -49,10 +46,6 @@ class DialogCategoryDetail extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
@@ -65,55 +58,61 @@ class DialogCategoryDetail extends LitElement {
|
||||
const entry = this._params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||
: this.hass.localize("ui.panel.config.category.editor.create")}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||
: this.hass.localize("ui.panel.config.category.editor.create")
|
||||
)}
|
||||
>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.required_error_msg"
|
||||
)}
|
||||
required
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.name"
|
||||
)}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.required_error_msg"
|
||||
)}
|
||||
required
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.category.editor.icon")}
|
||||
></ha-icon-picker>
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.category.editor.icon"
|
||||
)}
|
||||
></ha-icon-picker>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || !!this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ 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) {
|
||||
@@ -100,11 +101,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _getCategories = memoizeOne(
|
||||
(
|
||||
categories: CategoryRegistryEntry[] | undefined
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!categories) {
|
||||
return undefined;
|
||||
(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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const items = categories.map<PickerComboBoxItem>((category) => ({
|
||||
@@ -203,6 +210,10 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (value === NO_CATEGORIES_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import { dump } from "js-yaml";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task";
|
||||
import { fetchCategoryRegistry } from "../../../data/category_registry";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
type EntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { subscribeLabelRegistry } from "../../../data/label/label_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
|
||||
|
||||
export interface MetadataSuggestionResult {
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessedMetadataSuggestionResult {
|
||||
name: string;
|
||||
description?: string;
|
||||
categoryId?: string;
|
||||
labelIds?: string[];
|
||||
}
|
||||
|
||||
export interface MetadataSuggestionConfig {
|
||||
/** The domain to suggest metadata for (automation, script) */
|
||||
domain: "automation" | "script";
|
||||
/** The configuration to suggest metadata for */
|
||||
config: any;
|
||||
/** Whether to include description field in the suggestion */
|
||||
includeDescription?: boolean;
|
||||
/** Whether to include icon field in the suggestion (scripts only) */
|
||||
includeIcon?: boolean;
|
||||
}
|
||||
|
||||
type Categories = Record<string, string>;
|
||||
type Entities = Record<string, EntityRegistryEntry>;
|
||||
type Labels = Record<string, string>;
|
||||
|
||||
const fetchCategories = (
|
||||
connection: HomeAssistant["connection"],
|
||||
domain: MetadataSuggestionConfig["domain"]
|
||||
): Promise<Categories> =>
|
||||
fetchCategoryRegistry(connection, domain).then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
);
|
||||
|
||||
const fetchEntities = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Entities> =>
|
||||
subscribeOne(connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
);
|
||||
|
||||
const fetchLabels = (
|
||||
connection: HomeAssistant["connection"]
|
||||
): Promise<Labels> =>
|
||||
subscribeOne(connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
);
|
||||
|
||||
function buildMetadataInspirations(
|
||||
states: HomeAssistant["states"],
|
||||
entities: Record<string, EntityRegistryEntry>,
|
||||
categories: Categories,
|
||||
labels: Labels,
|
||||
domain: MetadataSuggestionConfig["domain"]
|
||||
): string[] {
|
||||
const inspirations: string[] = [];
|
||||
|
||||
for (const entity of Object.values(states)) {
|
||||
const entityEntry = entities[entity.entity_id];
|
||||
if (
|
||||
!entityEntry ||
|
||||
computeStateDomain(entity) !== domain ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
// Get the category for this domain
|
||||
const category = categories[entityEntry.categories[domain]];
|
||||
if (category) {
|
||||
inspiration += ` (category: ${category})`;
|
||||
}
|
||||
|
||||
if (entityEntry.labels.length) {
|
||||
inspiration += ` (labels: ${entityEntry.labels
|
||||
.map((label) => labels[label])
|
||||
.join(", ")})`;
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
}
|
||||
|
||||
return inspirations;
|
||||
}
|
||||
|
||||
export async function generateMetadataSuggestionTask(
|
||||
connection: HomeAssistant["connection"],
|
||||
states: HomeAssistant["states"],
|
||||
language: HomeAssistant["language"],
|
||||
suggestionConfig: MetadataSuggestionConfig
|
||||
): Promise<SuggestWithAIGenerateTask> {
|
||||
const { domain, config, includeDescription } = suggestionConfig;
|
||||
|
||||
let categories: Categories = {};
|
||||
let entities: Entities = {};
|
||||
let labels: Labels = {};
|
||||
try {
|
||||
[categories, entities, labels] = await Promise.all([
|
||||
fetchCategories(connection, domain),
|
||||
fetchEntities(connection),
|
||||
fetchLabels(connection),
|
||||
]);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error getting suggest metadata:", error);
|
||||
}
|
||||
|
||||
const inspirations = buildMetadataInspirations(
|
||||
states,
|
||||
entities,
|
||||
categories,
|
||||
labels,
|
||||
domain
|
||||
);
|
||||
|
||||
const structure: AITaskStructure = {
|
||||
name: {
|
||||
description: `The name of the ${domain}`,
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
...(includeDescription && {
|
||||
description: {
|
||||
description: `A short description of the ${domain}`,
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
}),
|
||||
labels: {
|
||||
description: `Labels for the ${domain}`,
|
||||
required: false,
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {
|
||||
description: `The category of the ${domain}`,
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: Object.entries(categories).map(([id, name]) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
type: "data",
|
||||
task: {
|
||||
task_name: `frontend__${domain}__save`,
|
||||
instructions: `Suggest in language "${language}" a name${includeDescription ? ", description" : ""}, category and labels for the following Home Assistant ${domain}.
|
||||
|
||||
The name should be relevant to the ${domain}'s purpose.
|
||||
${
|
||||
inspirations.length
|
||||
? `The name should be in same style and sentence capitalization as existing ${domain}s.
|
||||
Suggest a category and labels if relevant to the ${domain}'s purpose.
|
||||
Only suggest category and labels that are already used by existing ${domain}s.`
|
||||
: `The name should be short, descriptive, sentence case, and written in the language ${language}.`
|
||||
}${
|
||||
includeDescription
|
||||
? `
|
||||
If the ${domain} contains 5+ steps, include a short description.`
|
||||
: ""
|
||||
}
|
||||
|
||||
For inspiration, here are existing ${domain}s:
|
||||
${inspirations.join("\n")}
|
||||
|
||||
The ${domain} configuration is as follows:
|
||||
|
||||
${dump(config)}
|
||||
`,
|
||||
structure,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function processMetadataSuggestion(
|
||||
connection: HomeAssistant["connection"],
|
||||
domain: MetadataSuggestionConfig["domain"],
|
||||
result: GenDataTaskResult<MetadataSuggestionResult>
|
||||
): Promise<ProcessedMetadataSuggestionResult> {
|
||||
let categories: Categories = {};
|
||||
let labels: Labels = {};
|
||||
try {
|
||||
[categories, labels] = await Promise.all([
|
||||
fetchCategories(connection, domain),
|
||||
fetchLabels(connection),
|
||||
]);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error getting suggest metadata:", error);
|
||||
}
|
||||
|
||||
const processed: ProcessedMetadataSuggestionResult = {
|
||||
name: result.data.name,
|
||||
description: result.data.description ?? undefined,
|
||||
};
|
||||
|
||||
// Convert category name to ID
|
||||
if (result.data.category) {
|
||||
const categoryId = Object.entries(categories).find(
|
||||
([, name]) => name === result.data.category
|
||||
)?.[0];
|
||||
if (categoryId) {
|
||||
processed.categoryId = categoryId;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert label names to IDs
|
||||
if (result.data.labels?.length) {
|
||||
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
||||
result.data.labels.map((name) => [name, undefined])
|
||||
);
|
||||
let toFind = result.data.labels.length;
|
||||
for (const [labelId, labelName] of Object.entries(labels)) {
|
||||
if (labelName in newLabels && newLabels[labelName] === undefined) {
|
||||
newLabels[labelName] = labelId;
|
||||
toFind--;
|
||||
if (toFind === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foundLabels = Object.values(newLabels).filter(
|
||||
(labelId) => labelId !== undefined
|
||||
);
|
||||
if (foundLabels.length) {
|
||||
processed.labelIds = foundLabels;
|
||||
}
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiLocationEnter,
|
||||
mdiLocationExit,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } 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,
|
||||
@@ -23,16 +26,13 @@ import {
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesParameterized,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
} 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,11 +53,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
|
||||
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
@@ -77,86 +73,57 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-button-menu multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
.checked=${this._showSkipped}
|
||||
value="show_skipped"
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-dropdown-item>
|
||||
</ha-check-list-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
.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-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dropdown>
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
${canInstallUpdates.length
|
||||
? html`
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.isInstallable=${true}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</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>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<div class="no-updates">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.no_updates"
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
@@ -166,19 +133,27 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
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;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,14 +177,9 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private _filterInstallableUpdateEntities = memoizeOne(
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, false)
|
||||
);
|
||||
|
||||
private _filterNotInstallableUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, true)
|
||||
filterUpdateEntitiesWithInstall(entities, showSkipped)
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user