mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-11 18:47:24 +00:00
Compare commits
144 Commits
update-typ
...
remove-pad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
957bf875ae | ||
|
|
3804a4c7cb | ||
|
|
b823a3b139 | ||
|
|
47c9a407e6 | ||
|
|
c0ba48beb6 | ||
|
|
075e1df204 | ||
|
|
22c57853b4 | ||
|
|
fe824062a5 | ||
|
|
92bf9b4979 | ||
|
|
ac616a4d3d | ||
|
|
1aa1bfda2c | ||
|
|
38a5035d68 | ||
|
|
042cd0d3a3 | ||
|
|
00d708fbd4 | ||
|
|
852278e8aa | ||
|
|
15dcdffe55 | ||
|
|
0729aaacb8 | ||
|
|
92b8cd8f45 | ||
|
|
ad8d3dd598 | ||
|
|
d618c25095 | ||
|
|
83289bdd41 | ||
|
|
c7882f3926 | ||
|
|
7434b12d9f | ||
|
|
9081441d95 | ||
|
|
d63f610023 | ||
|
|
e069875432 | ||
|
|
91026b0986 | ||
|
|
4ec5fbc9a4 | ||
|
|
fb3a59272d | ||
|
|
9155c85509 | ||
|
|
9d74cd7561 | ||
|
|
22ddcca954 | ||
|
|
8f422357f1 | ||
|
|
44f5f7bdb5 | ||
|
|
83819a9be0 | ||
|
|
3c9dce20e2 | ||
|
|
a19e7002ea | ||
|
|
75608db9b8 | ||
|
|
08f30b714b | ||
|
|
9983129e26 | ||
|
|
40fe62c2ec | ||
|
|
d7660370ab | ||
|
|
bdad76937e | ||
|
|
d2822308ec | ||
|
|
99b94e799d | ||
|
|
1fb28df1a6 | ||
|
|
b4e8c56f58 | ||
|
|
17cc63deba | ||
|
|
b4f1c8755d | ||
|
|
b0d4c699db | ||
|
|
c07bf68161 | ||
|
|
d1a0eaece5 | ||
|
|
f608783551 | ||
|
|
dddba58d38 | ||
|
|
ebc16d6520 | ||
|
|
4ed8ecad01 | ||
|
|
c26fb1713d | ||
|
|
2b7b17625e | ||
|
|
cd3e4f55e2 | ||
|
|
1c12aea8f6 | ||
|
|
3722f971ca | ||
|
|
409f665641 | ||
|
|
5b3b17ef6d | ||
|
|
05b49e8c80 | ||
|
|
2dbdbb4b64 | ||
|
|
a825b632bf | ||
|
|
f8d706277d | ||
|
|
221bc732fb | ||
|
|
055c18463c | ||
|
|
ddd51ff097 | ||
|
|
55c75096d0 | ||
|
|
fd3502f3bc | ||
|
|
7d6bec01ae | ||
|
|
0aeb8fa75c | ||
|
|
456a44fdfd | ||
|
|
53a0b311de | ||
|
|
ded5ade0f2 | ||
|
|
5d2d6dcd6c | ||
|
|
92353ebed5 | ||
|
|
bc582db7fc | ||
|
|
ab415188ba | ||
|
|
29c11978b3 | ||
|
|
574f9e8936 | ||
|
|
fddc00bfab | ||
|
|
ff5cbb0613 | ||
|
|
498d933c06 | ||
|
|
f9fbb254bf | ||
|
|
536602580d | ||
|
|
c111bf1062 | ||
|
|
0242fbc6f8 | ||
|
|
f65a0ef4f7 | ||
|
|
6a333a4774 | ||
|
|
7742ccf631 | ||
|
|
20f2a8d53e | ||
|
|
ec9fbe7d77 | ||
|
|
6fa226d30a | ||
|
|
b76a723fd9 | ||
|
|
5237cc72b7 | ||
|
|
63d2718f67 | ||
|
|
1e26f155a7 | ||
|
|
27e13017c3 | ||
|
|
88f1dc9c16 | ||
|
|
825e707a80 | ||
|
|
0bcaa104e7 | ||
|
|
6b3f807129 | ||
|
|
c464d344db | ||
|
|
69f0a4a728 | ||
|
|
2ba8f9f99d | ||
|
|
7e06bbc467 | ||
|
|
6017d82c21 | ||
|
|
40c200a172 | ||
|
|
a2f70f682f | ||
|
|
c42a899b52 | ||
|
|
706f43e99e | ||
|
|
f5496c21e8 | ||
|
|
34dce5b279 | ||
|
|
a4f07423ec | ||
|
|
9e32c24f3c | ||
|
|
b281d095cd | ||
|
|
fe7e8e17ae | ||
|
|
2161357226 | ||
|
|
e8e65a4293 | ||
|
|
724adab2d6 | ||
|
|
345ad6c9c5 | ||
|
|
a88d066d7e | ||
|
|
a8e5c8482b | ||
|
|
d5ff8ab1e1 | ||
|
|
e765cc10fb | ||
|
|
916dec101f | ||
|
|
909fc119b7 | ||
|
|
8751dc46f4 | ||
|
|
118c25d25f | ||
|
|
ae5427a75e | ||
|
|
3b6e267fb5 | ||
|
|
1770a51303 | ||
|
|
534df3d378 | ||
|
|
23229b3e3b | ||
|
|
94ee99160b | ||
|
|
b009d71e8f | ||
|
|
2ab8209622 | ||
|
|
ed2940edc3 | ||
|
|
e2b9a06242 | ||
|
|
a7acee0438 | ||
|
|
1208af510c |
@@ -1,5 +1,5 @@
|
|||||||
import "./ha-gallery";
|
import "./ha-gallery";
|
||||||
|
|
||||||
import("../../src/resources/ha-style");
|
import("../../src/resources/append-ha-style");
|
||||||
|
|
||||||
document.body.appendChild(document.createElement("ha-gallery"));
|
document.body.appendChild(document.createElement("ha-gallery"));
|
||||||
|
|||||||
65
gallery/src/pages/components/ha-badge.markdown
Normal file
65
gallery/src/pages/components/ha-badge.markdown
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Badge
|
||||||
|
subtitle: Lovelace dashboard badge
|
||||||
|
---
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
# Badge `<ha-badge>`
|
||||||
|
|
||||||
|
The badge component is a small component that displays a number or status information. It is used in the lovelace dashboard on the top.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<ha-badge>
|
||||||
|
simple badge
|
||||||
|
</ha-badge>
|
||||||
|
|
||||||
|
<ha-badge label="Info">
|
||||||
|
With a label
|
||||||
|
</ha-badge>
|
||||||
|
|
||||||
|
<ha-badge type="button">
|
||||||
|
Type button
|
||||||
|
</ha-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<ha-badge> simple badge </ha-badge>
|
||||||
|
|
||||||
|
<ha-badge label="Info"> With a label </ha-badge>
|
||||||
|
|
||||||
|
<ha-badge type="button"> Type button </ha-badge>
|
||||||
|
```
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
**Slots**
|
||||||
|
|
||||||
|
- default slot is the content of the badge
|
||||||
|
- no default
|
||||||
|
- `icon` set the icon of the badge
|
||||||
|
- no default
|
||||||
|
|
||||||
|
**Properties/Attributes**
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| -------- | ----------------------- | ----------- | ------------------------------------------------------------ |
|
||||||
|
| type | `"badge"` or `"button"` | `"badge"` | If it's button it shows a ripple effect |
|
||||||
|
| label | string | `undefined` | Text label for the badge, only visible if `iconOnly = false` |
|
||||||
|
| iconOnly | boolean | `false` | Only show label |
|
||||||
|
|
||||||
|
**CSS Custom Properties**
|
||||||
|
|
||||||
|
- `--ha-badge-size` (default `36px`)
|
||||||
|
- `--ha-badge-border-radius` (default `calc(var(--ha-badge-size, 36px) / 2)`)
|
||||||
|
- `--ha-badge-font-size` (default `var(--ha-font-size-s)`)
|
||||||
|
- `--ha-badge-icon-size` (default `18px`)
|
||||||
129
gallery/src/pages/components/ha-badge.ts
Normal file
129
gallery/src/pages/components/ha-badge.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { mdiButtonCursor, mdiHome } from "@mdi/js";
|
||||||
|
import type { TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||||
|
import "../../../../src/components/ha-badge";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-svg-icon";
|
||||||
|
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
|
||||||
|
|
||||||
|
const badges: {
|
||||||
|
type?: "badge" | "button";
|
||||||
|
label?: string;
|
||||||
|
iconOnly?: boolean;
|
||||||
|
slot?: TemplateResult;
|
||||||
|
iconSlot?: TemplateResult;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
slot: html`<span>Badge</span>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "badge",
|
||||||
|
label: "Badge",
|
||||||
|
iconSlot: html`<ha-svg-icon slot="icon" .path=${mdiHome}></ha-svg-icon>`,
|
||||||
|
slot: html`<span>Badge</span>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
label: "Button",
|
||||||
|
iconSlot: html`<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.path=${mdiButtonCursor}
|
||||||
|
></ha-svg-icon>`,
|
||||||
|
slot: html`<span>Button</span>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
label: "Label only",
|
||||||
|
iconSlot: html`<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.path=${mdiButtonCursor}
|
||||||
|
></ha-svg-icon>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
label: "Label",
|
||||||
|
slot: html`<span>Button no label</span>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Icon only",
|
||||||
|
iconOnly: true,
|
||||||
|
iconSlot: html`<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.path=${mdiHomeAssistant}
|
||||||
|
></ha-svg-icon>`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-components-ha-badge")
|
||||||
|
export class DemoHaBadge extends LitElement {
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
${["light", "dark"].map(
|
||||||
|
(mode) => html`
|
||||||
|
<div class=${mode}>
|
||||||
|
<ha-card header="ha-badge ${mode} demo">
|
||||||
|
<div class="card-content">
|
||||||
|
${badges.map(
|
||||||
|
(badge) => html`
|
||||||
|
<ha-badge
|
||||||
|
.type=${badge.type || undefined}
|
||||||
|
.label=${badge.label}
|
||||||
|
.iconOnly=${badge.iconOnly || false}
|
||||||
|
>
|
||||||
|
${badge.iconSlot} ${badge.slot}
|
||||||
|
</ha-badge>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
applyThemesOnElement(
|
||||||
|
this.shadowRoot!.querySelector(".dark"),
|
||||||
|
{
|
||||||
|
default_theme: "default",
|
||||||
|
default_dark_theme: "default",
|
||||||
|
themes: {},
|
||||||
|
darkMode: true,
|
||||||
|
theme: "default",
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.dark,
|
||||||
|
.light {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
padding: 0 50px;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-components-ha-badge": DemoHaBadge;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
import "./ha-landing-page";
|
import "./ha-landing-page";
|
||||||
|
|
||||||
import("../../src/resources/ha-style");
|
import("../../src/resources/append-ha-style");
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -26,7 +26,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.27.0",
|
"@babel/runtime": "7.27.1",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
"@codemirror/commands": "6.8.1",
|
"@codemirror/commands": "6.8.1",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.1",
|
||||||
"@codemirror/search": "6.5.10",
|
"@codemirror/search": "6.5.10",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.36.6",
|
"@codemirror/view": "6.36.7",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||||
"@formatjs/intl-displaynames": "6.8.11",
|
"@formatjs/intl-displaynames": "6.8.11",
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.8.1",
|
"@tsparticles/engine": "3.8.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.7.4",
|
"@vaadin/combo-box": "24.7.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.7.4",
|
"@vaadin/vaadin-themable-mixin": "24.7.5",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"barcode-detector": "3.0.1",
|
"barcode-detector": "3.0.1",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.0",
|
||||||
"comlink": "4.4.2",
|
"comlink": "4.4.2",
|
||||||
"core-js": "3.41.0",
|
"core-js": "3.42.0",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"date-fns-tz": "3.2.0",
|
"date-fns-tz": "3.2.0",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"roboto-fontface": "0.10.0",
|
"roboto-fontface": "0.10.0",
|
||||||
"rrule": "2.8.1",
|
"rrule": "2.8.1",
|
||||||
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
|
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
@@ -150,18 +150,18 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.26.10",
|
"@babel/core": "7.27.1",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||||
"@babel/plugin-transform-runtime": "7.26.10",
|
"@babel/plugin-transform-runtime": "7.27.1",
|
||||||
"@babel/preset-env": "7.26.9",
|
"@babel/preset-env": "7.27.1",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.19.1",
|
"@bundle-stats/plugin-webpack-filter": "4.20.0",
|
||||||
"@lokalise/node-api": "14.4.0",
|
"@lokalise/node-api": "14.5.0",
|
||||||
"@octokit/auth-oauth-device": "7.1.5",
|
"@octokit/auth-oauth-device": "7.1.5",
|
||||||
"@octokit/plugin-retry": "7.2.1",
|
"@octokit/plugin-retry": "7.2.1",
|
||||||
"@octokit/rest": "21.1.1",
|
"@octokit/rest": "21.1.1",
|
||||||
"@rsdoctor/rspack-plugin": "1.0.2",
|
"@rsdoctor/rspack-plugin": "1.0.2",
|
||||||
"@rspack/cli": "1.3.7",
|
"@rspack/cli": "1.3.8",
|
||||||
"@rspack/core": "1.3.7",
|
"@rspack/core": "1.3.8",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.21",
|
"@types/chromecast-caf-receiver": "6.0.21",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.0",
|
"del": "8.0.0",
|
||||||
"eslint": "9.25.1",
|
"eslint": "9.26.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.2",
|
"eslint-config-prettier": "10.1.2",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
"eslint-plugin-lit": "2.1.1",
|
"eslint-plugin-lit": "2.1.1",
|
||||||
"eslint-plugin-lit-a11y": "4.1.4",
|
"eslint-plugin-lit-a11y": "4.1.4",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"eslint-plugin-wc": "3.0.0",
|
"eslint-plugin-wc": "3.0.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"glob": "11.0.2",
|
"glob": "11.0.2",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.31.0",
|
"typescript-eslint": "8.31.1",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.1.2",
|
"vitest": "3.1.2",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8284 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 37.9999L37.5 39.4999L76.9105 39.4999V37.9999V36.4999L37.5 36.4999L37.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||||
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
||||||
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
<mask id="mask0_2_779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
||||||
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_1110_23734)">
|
<g mask="url(#mask0_2_779)">
|
||||||
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
|
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||||
</g>
|
</g>
|
||||||
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
<path d="M85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999C82 36.343 83.3431 34.9999 85 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||||
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||||
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
|
||||||
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,19 +1,19 @@
|
|||||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
|
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<circle cx="47" cy="36" r="34" fill="white"/>
|
<circle cx="39" cy="36" r="34" fill="white"/>
|
||||||
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
||||||
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
|
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
||||||
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
|
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_1110_23775)">
|
<g mask="url(#mask0_2_810)">
|
||||||
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
|
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||||
</g>
|
</g>
|
||||||
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||||
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1,19 +1,18 @@
|
|||||||
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
|
<path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
|
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
||||||
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
<path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
||||||
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
|
<path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
|
||||||
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
|
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_1180_4965)">
|
<g mask="url(#mask0_2_810)">
|
||||||
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
|
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||||
</g>
|
</g>
|
||||||
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
<path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/>
|
||||||
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
|
||||||
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
<path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20250326.0"
|
version = "20250430.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*"]
|
license-files = ["LICENSE*"]
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type { LitElement } from "lit";
|
|||||||
*/
|
*/
|
||||||
export interface DragScrollControllerConfig {
|
export interface DragScrollControllerConfig {
|
||||||
selector: string;
|
selector: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
trackScroll?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DragScrollController implements ReactiveController {
|
export class DragScrollController implements ReactiveController {
|
||||||
@@ -22,38 +24,109 @@ export class DragScrollController implements ReactiveController {
|
|||||||
|
|
||||||
public scrollLeft = 0;
|
public scrollLeft = 0;
|
||||||
|
|
||||||
|
public scrolledStart = false;
|
||||||
|
|
||||||
|
public scrolledEnd = false;
|
||||||
|
|
||||||
private _host: ReactiveControllerHost & LitElement;
|
private _host: ReactiveControllerHost & LitElement;
|
||||||
|
|
||||||
private _selector: string;
|
private _selector: string;
|
||||||
|
|
||||||
private _scrollContainer?: HTMLElement | null;
|
private _scrollContainer?: HTMLElement | null;
|
||||||
|
|
||||||
|
private _enabled = true;
|
||||||
|
|
||||||
|
private _trackScroll = false;
|
||||||
|
|
||||||
|
public get enabled(): boolean {
|
||||||
|
return this._enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set enabled(value: boolean) {
|
||||||
|
if (value === this._enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._enabled = value;
|
||||||
|
if (this._enabled) {
|
||||||
|
this._attach();
|
||||||
|
} else {
|
||||||
|
this._detach();
|
||||||
|
}
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
host: ReactiveControllerHost & LitElement,
|
host: ReactiveControllerHost & LitElement,
|
||||||
{ selector }: DragScrollControllerConfig
|
{ selector, enabled, trackScroll }: DragScrollControllerConfig
|
||||||
) {
|
) {
|
||||||
this._selector = selector;
|
this._selector = selector;
|
||||||
this._host = host;
|
this._host = host;
|
||||||
|
this._trackScroll = trackScroll ?? false;
|
||||||
|
this.enabled = enabled ?? true;
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
hostUpdated() {
|
hostUpdated() {
|
||||||
if (this._scrollContainer) {
|
if (!this.enabled || this._scrollContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._attach();
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDisconnected() {
|
||||||
|
this._detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attach() {
|
||||||
this._scrollContainer = this._host.renderRoot?.querySelector(
|
this._scrollContainer = this._host.renderRoot?.querySelector(
|
||||||
this._selector
|
this._selector
|
||||||
);
|
);
|
||||||
if (this._scrollContainer) {
|
if (this._scrollContainer) {
|
||||||
this._scrollContainer.addEventListener("mousedown", this._mouseDown);
|
this._scrollContainer.addEventListener("mousedown", this._mouseDown);
|
||||||
|
if (this._trackScroll) {
|
||||||
|
this._scrollContainer.addEventListener("scroll", this._onScroll);
|
||||||
|
this.scrolledStart = this._scrollContainer.scrollLeft > 0;
|
||||||
|
this.scrolledEnd =
|
||||||
|
this._scrollContainer.scrollLeft + this._scrollContainer.offsetWidth <
|
||||||
|
this._scrollContainer.scrollWidth;
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hostDisconnected() {
|
private _detach() {
|
||||||
window.removeEventListener("mousemove", this._mouseMove);
|
window.removeEventListener("mousemove", this._mouseMove);
|
||||||
window.removeEventListener("mouseup", this._mouseUp);
|
window.removeEventListener("mouseup", this._mouseUp);
|
||||||
|
if (this._scrollContainer) {
|
||||||
|
this._scrollContainer.removeEventListener("mousedown", this._mouseDown);
|
||||||
|
this._scrollContainer.removeEventListener("scroll", this._onScroll);
|
||||||
|
this._scrollContainer = undefined;
|
||||||
|
}
|
||||||
|
this.scrolled = false;
|
||||||
|
this.scrolling = false;
|
||||||
|
this.scrolledStart = false;
|
||||||
|
this.scrolledEnd = false;
|
||||||
|
this.mouseIsDown = false;
|
||||||
|
this.scrollStartX = 0;
|
||||||
|
this.scrollLeft = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onScroll = (event: Event) => {
|
||||||
|
const oldScrolledStart = this.scrolledStart;
|
||||||
|
const oldScrolledEnd = this.scrolledEnd;
|
||||||
|
|
||||||
|
const container = event.currentTarget as HTMLElement;
|
||||||
|
this.scrolledStart = container.scrollLeft > 0;
|
||||||
|
this.scrolledEnd =
|
||||||
|
container.scrollLeft + container.offsetWidth < container.scrollWidth;
|
||||||
|
if (
|
||||||
|
this.scrolledStart !== oldScrolledStart ||
|
||||||
|
this.scrolledEnd !== oldScrolledEnd
|
||||||
|
) {
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private _mouseDown = (event: MouseEvent) => {
|
private _mouseDown = (event: MouseEvent) => {
|
||||||
const scrollContainer = this._scrollContainer;
|
const scrollContainer = this._scrollContainer;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { ReactiveElement } from "lit";
|
import type { ReactiveElement } from "lit";
|
||||||
import type { InternalPropertyDeclaration } from "lit/decorators";
|
|
||||||
|
|
||||||
type Callback = (oldValue: any, newValue: any) => void;
|
type Callback = (oldValue: any, newValue: any) => void;
|
||||||
|
|
||||||
@@ -108,7 +107,6 @@ export function storage(options: {
|
|||||||
storage?: "localStorage" | "sessionStorage";
|
storage?: "localStorage" | "sessionStorage";
|
||||||
subscribe?: boolean;
|
subscribe?: boolean;
|
||||||
state?: boolean;
|
state?: boolean;
|
||||||
stateOptions?: InternalPropertyDeclaration;
|
|
||||||
serializer?: (value: any) => any;
|
serializer?: (value: any) => any;
|
||||||
deserializer?: (value: any) => any;
|
deserializer?: (value: any) => any;
|
||||||
}) {
|
}) {
|
||||||
@@ -174,7 +172,7 @@ export function storage(options: {
|
|||||||
performUpdate.call(this);
|
performUpdate.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.state && options.subscribe) {
|
if (options.subscribe) {
|
||||||
const connectedCallback = proto.connectedCallback;
|
const connectedCallback = proto.connectedCallback;
|
||||||
const disconnectedCallback = proto.disconnectedCallback;
|
const disconnectedCallback = proto.disconnectedCallback;
|
||||||
|
|
||||||
@@ -192,12 +190,6 @@ export function storage(options: {
|
|||||||
el.__unbsubLocalStorage = undefined;
|
el.__unbsubLocalStorage = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (options.state) {
|
|
||||||
ReactiveElement.createProperty(propertyKey, {
|
|
||||||
noAccessor: true,
|
|
||||||
...options.stateOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
|
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
|
||||||
let newDescriptor: PropertyDescriptor;
|
let newDescriptor: PropertyDescriptor;
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import type { ReactiveElement, PropertyValues } from "lit";
|
||||||
ReactiveElement,
|
|
||||||
type PropertyDeclaration,
|
|
||||||
type PropertyValues,
|
|
||||||
} from "lit";
|
|
||||||
import { shallowEqual } from "../util/shallow-equal";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform function type.
|
* Transform function type.
|
||||||
*/
|
*/
|
||||||
@@ -23,7 +17,6 @@ type ReactiveTransformElement = ReactiveElement & {
|
|||||||
export function transform<T, V>(config: {
|
export function transform<T, V>(config: {
|
||||||
transformer: Transformer<T, V>;
|
transformer: Transformer<T, V>;
|
||||||
watch?: PropertyKey[];
|
watch?: PropertyKey[];
|
||||||
propertyOptions?: PropertyDeclaration;
|
|
||||||
}) {
|
}) {
|
||||||
return <ElemClass extends ReactiveElement>(
|
return <ElemClass extends ReactiveElement>(
|
||||||
proto: ElemClass,
|
proto: ElemClass,
|
||||||
@@ -84,11 +77,6 @@ export function transform<T, V>(config: {
|
|||||||
curWatch.add(propertyKey);
|
curWatch.add(propertyKey);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ReactiveElement.createProperty(propertyKey, {
|
|
||||||
noAccessor: true,
|
|
||||||
hasChanged: (v: any, o: any) => !shallowEqual(v, o),
|
|
||||||
...config.propertyOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
|
const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey);
|
||||||
let newDescriptor: PropertyDescriptor;
|
let newDescriptor: PropertyDescriptor;
|
||||||
|
|||||||
36
src/common/dom/can-override-input.ts
Normal file
36
src/common/dom/can-override-input.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
|
||||||
|
if (
|
||||||
|
composedPath.some(
|
||||||
|
(el) =>
|
||||||
|
"tagName" in el &&
|
||||||
|
(el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = composedPath[0] as Element;
|
||||||
|
|
||||||
|
if (el.tagName === "TEXTAREA") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.parentElement?.tagName === "HA-SELECT") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.tagName !== "INPUT") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ((el as HTMLInputElement).type) {
|
||||||
|
case "button":
|
||||||
|
case "checkbox":
|
||||||
|
case "hidden":
|
||||||
|
case "radio":
|
||||||
|
case "range":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,34 +6,15 @@ interface AreaContext {
|
|||||||
area: AreaRegistryEntry | null;
|
area: AreaRegistryEntry | null;
|
||||||
floor: FloorRegistryEntry | null;
|
floor: FloorRegistryEntry | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the context of a specific area, including its associated area registry entry
|
|
||||||
* and floor registry entry, if available.
|
|
||||||
*
|
|
||||||
* @param areaId - The unique identifier of the area to retrieve context for.
|
|
||||||
* @param hass - The Home Assistant instance containing area and floor registry data.
|
|
||||||
* @returns An object containing the area registry entry and the associated floor registry entry,
|
|
||||||
* or `null` values if the area or floor is not found.
|
|
||||||
*/
|
|
||||||
export const getAreaContext = (
|
export const getAreaContext = (
|
||||||
areaId: string,
|
area: AreaRegistryEntry,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
): AreaContext => {
|
): AreaContext => {
|
||||||
const area = (hass.areas[areaId] as AreaRegistryEntry | undefined) || null;
|
const floorId = area.floor_id;
|
||||||
|
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||||
if (!area) {
|
|
||||||
return {
|
|
||||||
area: null,
|
|
||||||
floor: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const floorId = area?.floor_id;
|
|
||||||
const floor = floorId ? hass.floors[floorId] : null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
area: area,
|
area: area,
|
||||||
floor: floor,
|
floor: floor || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/common/entity/context/get_device_context.ts
Normal file
26
src/common/entity/context/get_device_context.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
interface DeviceContext {
|
||||||
|
device: DeviceRegistryEntry;
|
||||||
|
area: AreaRegistryEntry | null;
|
||||||
|
floor: FloorRegistryEntry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeviceContext = (
|
||||||
|
device: DeviceRegistryEntry,
|
||||||
|
hass: HomeAssistant
|
||||||
|
): DeviceContext => {
|
||||||
|
const areaId = device.area_id;
|
||||||
|
const area = areaId ? hass.areas[areaId] : undefined;
|
||||||
|
const floorId = area?.floor_id;
|
||||||
|
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
device: device,
|
||||||
|
area: area || null,
|
||||||
|
floor: floor || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
|
import type {
|
||||||
|
EntityRegistryDisplayEntry,
|
||||||
|
EntityRegistryEntry,
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
@@ -11,27 +16,15 @@ interface EntityContext {
|
|||||||
floor: FloorRegistryEntry | null;
|
floor: FloorRegistryEntry | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the context of an entity, including its associated device, area, and floor.
|
|
||||||
*
|
|
||||||
* @param entityId - The unique identifier of the entity to retrieve the context for.
|
|
||||||
* @param hass - The Home Assistant object containing the registry data for entities, devices, areas, and floors.
|
|
||||||
* @returns An object containing the entity, its associated device, area, and floor, or `null` for each if not found.
|
|
||||||
*
|
|
||||||
* The returned `EntityContext` object includes:
|
|
||||||
* - `entity`: The entity registry entry, or `null` if the entity is not found.
|
|
||||||
* - `device`: The device registry entry associated with the entity, or `null` if not found.
|
|
||||||
* - `area`: The area registry entry associated with the entity or device, or `null` if not found.
|
|
||||||
* - `floor`: The floor registry entry associated with the area, or `null` if not found.
|
|
||||||
*/
|
|
||||||
export const getEntityContext = (
|
export const getEntityContext = (
|
||||||
entityId: string,
|
stateObj: HassEntity,
|
||||||
hass: HomeAssistant
|
hass: HomeAssistant
|
||||||
): EntityContext => {
|
): EntityContext => {
|
||||||
const entity =
|
const entry = hass.entities[stateObj.entity_id] as
|
||||||
(hass.entities[entityId] as EntityRegistryDisplayEntry | undefined) || null;
|
| EntityRegistryDisplayEntry
|
||||||
|
| undefined;
|
||||||
|
|
||||||
if (!entity) {
|
if (!entry) {
|
||||||
return {
|
return {
|
||||||
entity: null,
|
entity: null,
|
||||||
device: null,
|
device: null,
|
||||||
@@ -39,18 +32,28 @@ export const getEntityContext = (
|
|||||||
floor: null,
|
floor: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return getEntityEntryContext(entry, hass);
|
||||||
|
};
|
||||||
|
|
||||||
const deviceId = entity?.device_id;
|
export const getEntityEntryContext = (
|
||||||
const device = deviceId ? hass.devices[deviceId] : null;
|
entry:
|
||||||
const areaId = entity?.area_id || device?.area_id;
|
| EntityRegistryDisplayEntry
|
||||||
const area = areaId ? hass.areas[areaId] : null;
|
| EntityRegistryEntry
|
||||||
|
| ExtEntityRegistryEntry,
|
||||||
|
hass: HomeAssistant
|
||||||
|
): EntityContext => {
|
||||||
|
const entity = hass.entities[entry.entity_id];
|
||||||
|
const deviceId = entry?.device_id;
|
||||||
|
const device = deviceId ? hass.devices[deviceId] : undefined;
|
||||||
|
const areaId = entry?.area_id || device?.area_id;
|
||||||
|
const area = areaId ? hass.areas[areaId] : undefined;
|
||||||
const floorId = area?.floor_id;
|
const floorId = area?.floor_id;
|
||||||
const floor = floorId ? hass.floors[floorId] : null;
|
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entity: entity,
|
entity: entity,
|
||||||
device: device,
|
device: device || null,
|
||||||
area: area,
|
area: area || null,
|
||||||
floor: floor,
|
floor: floor || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const generateEntityFilter = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { area, floor, device, entity } = getEntityContext(entityId, hass);
|
const { area, floor, device, entity } = getEntityContext(stateObj, hass);
|
||||||
|
|
||||||
if (entity && entity.hidden) {
|
if (entity && entity.hidden) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
|
||||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
interface AreaContext {
|
|
||||||
floor: FloorRegistryEntry | null;
|
|
||||||
}
|
|
||||||
export const getAreaContext = (
|
|
||||||
area: AreaRegistryEntry,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): AreaContext => {
|
|
||||||
const floorId = area.floor_id;
|
|
||||||
const floor = floorId ? hass.floors[floorId] : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
floor: floor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
|
||||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
|
||||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
interface DeviceContext {
|
|
||||||
area: AreaRegistryEntry | null;
|
|
||||||
floor: FloorRegistryEntry | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDeviceContext = (
|
|
||||||
device: DeviceRegistryEntry,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): DeviceContext => {
|
|
||||||
const areaId = device.area_id;
|
|
||||||
const area = areaId ? hass.areas[areaId] : null;
|
|
||||||
const floorId = area?.floor_id;
|
|
||||||
const floor = floorId ? hass.floors[floorId] : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
area: area,
|
|
||||||
floor: floor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
|
||||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
|
||||||
import type {
|
|
||||||
EntityRegistryDisplayEntry,
|
|
||||||
EntityRegistryEntry,
|
|
||||||
ExtEntityRegistryEntry,
|
|
||||||
} from "../../data/entity_registry";
|
|
||||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
interface EntityContext {
|
|
||||||
device: DeviceRegistryEntry | null;
|
|
||||||
area: AreaRegistryEntry | null;
|
|
||||||
floor: FloorRegistryEntry | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getEntityContext = (
|
|
||||||
stateObj: HassEntity,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): EntityContext => {
|
|
||||||
const entry = hass.entities[stateObj.entity_id] as
|
|
||||||
| EntityRegistryDisplayEntry
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
return {
|
|
||||||
device: null,
|
|
||||||
area: null,
|
|
||||||
floor: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return getEntityEntryContext(entry, hass);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEntityEntryContext = (
|
|
||||||
entry:
|
|
||||||
| EntityRegistryDisplayEntry
|
|
||||||
| EntityRegistryEntry
|
|
||||||
| ExtEntityRegistryEntry,
|
|
||||||
hass: HomeAssistant
|
|
||||||
): EntityContext => {
|
|
||||||
const deviceId = entry?.device_id;
|
|
||||||
const device = deviceId ? hass.devices[deviceId] : null;
|
|
||||||
const areaId = entry?.area_id || device?.area_id;
|
|
||||||
const area = areaId ? hass.areas[areaId] : null;
|
|
||||||
const floorId = area?.floor_id;
|
|
||||||
const floor = floorId ? hass.floors[floorId] : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
device: device,
|
|
||||||
area: area,
|
|
||||||
floor: floor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -600,12 +600,32 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getSeries() {
|
private _getSeries() {
|
||||||
if (!Array.isArray(this.data)) {
|
const series = ensureArray(this.data).filter(
|
||||||
return this.data;
|
|
||||||
}
|
|
||||||
return this.data.filter(
|
|
||||||
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
|
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
|
||||||
);
|
);
|
||||||
|
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||||
|
| YAXisOption
|
||||||
|
| undefined;
|
||||||
|
if (yAxis?.type === "log") {
|
||||||
|
// set <=0 values to null so they render as gaps on a log graph
|
||||||
|
return series.map((d) =>
|
||||||
|
d.type === "line"
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
data: d.data?.map((v) =>
|
||||||
|
Array.isArray(v)
|
||||||
|
? [
|
||||||
|
v[0],
|
||||||
|
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
|
||||||
|
...v.slice(2),
|
||||||
|
]
|
||||||
|
: v
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDefaultHeight() {
|
private _getDefaultHeight() {
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ export class HaSankeyChart extends LitElement {
|
|||||||
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
|
||||||
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
const filteredNodes = data.nodes.filter((n) => n.value > 0);
|
||||||
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
|
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
|
||||||
|
const depthMap = new Map<number, number>();
|
||||||
|
indexes.sort().forEach((index, i) => {
|
||||||
|
depthMap.set(index, i);
|
||||||
|
});
|
||||||
const links = this._processLinks(filteredNodes, data.links);
|
const links = this._processLinks(filteredNodes, data.links);
|
||||||
const sectionWidth = width / indexes.length;
|
const sectionWidth = width / indexes.length;
|
||||||
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
|
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
|
||||||
@@ -119,7 +123,7 @@ export class HaSankeyChart extends LitElement {
|
|||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: node.color,
|
color: node.color,
|
||||||
},
|
},
|
||||||
depth: node.index,
|
depth: depthMap.get(node.index),
|
||||||
})),
|
})),
|
||||||
links,
|
links,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ export class HaDataTable extends LitElement {
|
|||||||
.map(
|
.map(
|
||||||
([key2, column2], i) =>
|
([key2, column2], i) =>
|
||||||
html`${i !== 0
|
html`${i !== 0
|
||||||
? " ⸱ "
|
? " · "
|
||||||
: nothing}${column2.template
|
: nothing}${column2.template
|
||||||
? column2.template(row)
|
? column2.template(row)
|
||||||
: row[key2]}`
|
: row[key2]}`
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
|
import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
|
||||||
import "./ha-entity-picker";
|
import "./ha-entity-picker";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
|
|
||||||
|
|
||||||
@customElement("ha-entities-picker")
|
@customElement("ha-entities-picker")
|
||||||
class HaEntitiesPickerLight extends LitElement {
|
class HaEntitiesPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Array }) public value?: string[];
|
@property({ type: Array }) public value?: string[];
|
||||||
@@ -17,6 +17,10 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,13 +71,8 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
@property({ type: Array, attribute: "exclude-entities" })
|
@property({ type: Array, attribute: "exclude-entities" })
|
||||||
public excludeEntities?: string[];
|
public excludeEntities?: string[];
|
||||||
|
|
||||||
@property({ attribute: "picked-entity-label" })
|
|
||||||
public pickedEntityLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||||
|
|
||||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||||
|
|
||||||
@@ -84,6 +83,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
|
|
||||||
const currentEntities = this._currentEntities;
|
const currentEntities = this._currentEntities;
|
||||||
return html`
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
${currentEntities.map(
|
${currentEntities.map(
|
||||||
(entityId) => html`
|
(entityId) => html`
|
||||||
<div>
|
<div>
|
||||||
@@ -99,7 +99,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||||
.entityFilter=${this.entityFilter}
|
.entityFilter=${this.entityFilter}
|
||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedEntityLabel}
|
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.createDomains=${this.createDomains}
|
.createDomains=${this.createDomains}
|
||||||
@value-changed=${this._entityChanged}
|
@value-changed=${this._entityChanged}
|
||||||
@@ -121,7 +120,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||||
.entityFilter=${this.entityFilter}
|
.entityFilter=${this.entityFilter}
|
||||||
.label=${this.pickEntityLabel}
|
.placeholder=${this.placeholder}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.createDomains=${this.createDomains}
|
.createDomains=${this.createDomains}
|
||||||
@@ -198,11 +197,15 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
div {
|
div {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-entities-picker": HaEntitiesPickerLight;
|
"ha-entities-picker": HaEntitiesPicker;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,16 +73,20 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value
|
.value=${this.value
|
||||||
? computeAttributeNameDisplay(
|
? stateObj
|
||||||
this.hass.localize,
|
? computeAttributeNameDisplay(
|
||||||
this.hass.states[this.entityId!],
|
this.hass.localize,
|
||||||
this.hass.entities,
|
stateObj,
|
||||||
this.value
|
this.hass.entities,
|
||||||
)
|
this.value
|
||||||
|
)
|
||||||
|
: this.value
|
||||||
: ""}
|
: ""}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label ??
|
.label=${this.label ??
|
||||||
|
|||||||
510
src/components/entity/ha-entity-combo-box.ts
Normal file
510
src/components/entity/ha-entity-combo-box.ts
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
import { mdiMagnify, mdiPlus } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
|
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import type { HelperDomain } from "../../panels/config/helpers/const";
|
||||||
|
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||||
|
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||||
|
import { HaFuse } from "../../resources/fuse";
|
||||||
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
|
import "../ha-combo-box";
|
||||||
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
import "../ha-combo-box-item";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
import "../ha-svg-icon";
|
||||||
|
import "./state-badge";
|
||||||
|
|
||||||
|
interface EntityComboBoxItem {
|
||||||
|
// Force empty label to always display empty value by default in the search field
|
||||||
|
id: string;
|
||||||
|
label: "";
|
||||||
|
primary: string;
|
||||||
|
secondary?: string;
|
||||||
|
domain_name?: string;
|
||||||
|
search_labels?: string[];
|
||||||
|
sorting_label?: string;
|
||||||
|
icon_path?: string;
|
||||||
|
stateObj?: HassEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
const CREATE_ID = "___create-new-entity___";
|
||||||
|
const NO_ENTITIES_ID = "___no-entities___";
|
||||||
|
|
||||||
|
@customElement("ha-entity-combo-box")
|
||||||
|
export class HaEntityComboBox extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
|
public allowCustomEntity;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only entities with these unit of measuments.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-unit-of-measurement
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-unit-of-measurement" })
|
||||||
|
public includeUnitOfMeasurement?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of allowed entities to show.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-entities
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-entities" })
|
||||||
|
public includeEntities?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of entities to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-entities
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-entities" })
|
||||||
|
public excludeEntities?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
|
public hideClearIcon = false;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _initialItems = false;
|
||||||
|
|
||||||
|
private _items: EntityComboBoxItem[] = [];
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this.hass.loadBackendTranslation("title");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||||
|
item,
|
||||||
|
{ index }
|
||||||
|
) => {
|
||||||
|
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||||
|
${item.icon_path
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<state-badge
|
||||||
|
slot="start"
|
||||||
|
.stateObj=${item.stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></state-badge>
|
||||||
|
`}
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
${item.secondary
|
||||||
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
|
: nothing}
|
||||||
|
${item.stateObj && showEntityId
|
||||||
|
? html`
|
||||||
|
<span slot="supporting-text" class="code">
|
||||||
|
${item.stateObj.entity_id}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${item.domain_name && !showEntityId
|
||||||
|
? html`
|
||||||
|
<div slot="trailing-supporting-text">${item.domain_name}</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getItems = memoizeOne(
|
||||||
|
(
|
||||||
|
_opened: boolean,
|
||||||
|
hass: this["hass"],
|
||||||
|
includeDomains: this["includeDomains"],
|
||||||
|
excludeDomains: this["excludeDomains"],
|
||||||
|
entityFilter: this["entityFilter"],
|
||||||
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
|
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||||
|
includeEntities: this["includeEntities"],
|
||||||
|
excludeEntities: this["excludeEntities"],
|
||||||
|
createDomains: this["createDomains"]
|
||||||
|
): EntityComboBoxItem[] => {
|
||||||
|
let items: EntityComboBoxItem[] = [];
|
||||||
|
|
||||||
|
let entityIds = Object.keys(hass.states);
|
||||||
|
|
||||||
|
const createItems = createDomains?.length
|
||||||
|
? createDomains.map((domain) => {
|
||||||
|
const primary = hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.create_helper",
|
||||||
|
{
|
||||||
|
domain: isHelperDomain(domain)
|
||||||
|
? hass.localize(
|
||||||
|
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||||
|
)
|
||||||
|
: domainToName(hass.localize, domain),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: CREATE_ID + domain,
|
||||||
|
label: "",
|
||||||
|
primary: primary,
|
||||||
|
secondary: this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.new_entity"
|
||||||
|
),
|
||||||
|
icon_path: mdiPlus,
|
||||||
|
} satisfies EntityComboBoxItem;
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!entityIds.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: NO_ENTITIES_ID,
|
||||||
|
label: "",
|
||||||
|
primary: this.hass!.localize(
|
||||||
|
"ui.components.entity.entity-picker.no_entities"
|
||||||
|
),
|
||||||
|
icon_path: mdiMagnify,
|
||||||
|
},
|
||||||
|
...createItems,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeEntities) {
|
||||||
|
entityIds = entityIds.filter((entityId) =>
|
||||||
|
includeEntities.includes(entityId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeEntities) {
|
||||||
|
entityIds = entityIds.filter(
|
||||||
|
(entityId) => !excludeEntities.includes(entityId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDomains) {
|
||||||
|
entityIds = entityIds.filter((eid) =>
|
||||||
|
includeDomains.includes(computeDomain(eid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
|
entityIds = entityIds.filter(
|
||||||
|
(eid) => !excludeDomains.includes(computeDomain(eid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
|
items = entityIds
|
||||||
|
.map<EntityComboBoxItem>((entityId) => {
|
||||||
|
const stateObj = hass!.states[entityId];
|
||||||
|
|
||||||
|
const { area, device } = getEntityContext(stateObj, hass);
|
||||||
|
|
||||||
|
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||||
|
const entityName = computeEntityName(stateObj, hass);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const domainName = domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
computeDomain(entityId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || entityId;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: entityId,
|
||||||
|
label: "",
|
||||||
|
primary: primary,
|
||||||
|
secondary: secondary,
|
||||||
|
domain_name: domainName,
|
||||||
|
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
||||||
|
search_labels: [
|
||||||
|
entityName,
|
||||||
|
deviceName,
|
||||||
|
areaName,
|
||||||
|
domainName,
|
||||||
|
friendlyName,
|
||||||
|
entityId,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
|
stateObj: stateObj,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((entityA, entityB) =>
|
||||||
|
caseInsensitiveStringCompare(
|
||||||
|
entityA.sorting_label!,
|
||||||
|
entityB.sorting_label!,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === this.value ||
|
||||||
|
(item.stateObj?.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(
|
||||||
|
item.stateObj.attributes.device_class
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeUnitOfMeasurement) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === this.value ||
|
||||||
|
(item.stateObj?.attributes.unit_of_measurement &&
|
||||||
|
includeUnitOfMeasurement.includes(
|
||||||
|
item.stateObj.attributes.unit_of_measurement
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
items = items.filter(
|
||||||
|
(item) =>
|
||||||
|
// We always want to include the entity of the current value
|
||||||
|
item.id === this.value ||
|
||||||
|
(item.stateObj && entityFilter!(item.stateObj))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: NO_ENTITIES_ID,
|
||||||
|
label: "",
|
||||||
|
primary: this.hass!.localize(
|
||||||
|
"ui.components.entity.entity-picker.no_match"
|
||||||
|
),
|
||||||
|
icon_path: mdiMagnify,
|
||||||
|
},
|
||||||
|
...createItems,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createItems?.length) {
|
||||||
|
items.push(...createItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
changedProps.has("value") ||
|
||||||
|
changedProps.has("label") ||
|
||||||
|
changedProps.has("disabled")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
|
||||||
|
this._items = this._getItems(
|
||||||
|
this._opened,
|
||||||
|
this.hass,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.entityFilter,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.includeUnitOfMeasurement,
|
||||||
|
this.includeEntities,
|
||||||
|
this.excludeEntities,
|
||||||
|
this.createDomains
|
||||||
|
);
|
||||||
|
if (this._initialItems) {
|
||||||
|
this.comboBox.filteredItems = this._items;
|
||||||
|
}
|
||||||
|
this._initialItems = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
item-value-path="id"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._value}
|
||||||
|
.label=${this.label === undefined
|
||||||
|
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
||||||
|
: this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
|
.filteredItems=${this._items}
|
||||||
|
.renderer=${this._rowRenderer}
|
||||||
|
.required=${this.required}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
// Clear the input field to prevent showing the old value next time
|
||||||
|
this.comboBox.setTextFieldValue("");
|
||||||
|
const newValue = ev.detail.value?.trim();
|
||||||
|
|
||||||
|
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||||
|
const domain = newValue.substring(CREATE_ID.length);
|
||||||
|
showHelperDetailDialog(this, {
|
||||||
|
domain,
|
||||||
|
dialogClosedCallback: (item) => {
|
||||||
|
if (item.entityId) this._setValue(item.entityId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
|
||||||
|
Fuse.createIndex(["search_labels"], states)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
if (!this._opened) return;
|
||||||
|
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||||
|
|
||||||
|
const index = this._fuseIndex(this._items);
|
||||||
|
const fuse = new HaFuse(this._items, {}, index);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(filterString);
|
||||||
|
if (results) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
target.filteredItems = [
|
||||||
|
{
|
||||||
|
id: NO_ENTITIES_ID,
|
||||||
|
label: "",
|
||||||
|
primary: this.hass!.localize(
|
||||||
|
"ui.components.entity.entity-picker.no_match"
|
||||||
|
),
|
||||||
|
icon_path: mdiMagnify,
|
||||||
|
},
|
||||||
|
] as EntityComboBoxItem[];
|
||||||
|
} else {
|
||||||
|
target.filteredItems = results.map((result) => result.item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.filteredItems = this._items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string | undefined) {
|
||||||
|
if (!value || !isValidEntityId(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-entity-combo-box": HaEntityComboBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +1,27 @@
|
|||||||
import { mdiMagnify, mdiPlus } from "@mdi/js";
|
import { mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||||
import Fuse from "fuse.js";
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import type { PropertyValues, TemplateResult } from "lit";
|
|
||||||
import { html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
|
||||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import { getEntityContext } from "../../common/entity/get_entity_context";
|
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import { domainToName } from "../../data/integration";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import type { HelperDomain } from "../../panels/config/helpers/const";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
|
||||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
|
||||||
import { HaFuse } from "../../resources/fuse";
|
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|
||||||
import "../ha-combo-box";
|
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
|
import type { HaMdListItem } from "../ha-md-list-item";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
|
import "./ha-entity-combo-box";
|
||||||
|
import type {
|
||||||
|
HaEntityComboBox,
|
||||||
|
HaEntityComboBoxEntityFilterFunc,
|
||||||
|
} from "./ha-entity-combo-box";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
|
||||||
const FAKE_ENTITY: HassEntity = {
|
|
||||||
entity_id: "",
|
|
||||||
state: "",
|
|
||||||
last_changed: "",
|
|
||||||
last_updated: "",
|
|
||||||
context: { id: "", user_id: null, parent_id: null },
|
|
||||||
attributes: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface EntityPickerItem extends HassEntity {
|
|
||||||
label: string;
|
|
||||||
primary: string;
|
|
||||||
secondary?: string;
|
|
||||||
translated_domain?: string;
|
|
||||||
show_entity_id?: boolean;
|
|
||||||
entity_name?: string;
|
|
||||||
area_name?: string;
|
|
||||||
device_name?: string;
|
|
||||||
friendly_name?: string;
|
|
||||||
sorting_label?: string;
|
|
||||||
icon_path?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
|
||||||
|
|
||||||
const CREATE_ID = "___create-new-entity___";
|
|
||||||
|
|
||||||
const DOMAIN_STYLE = styleMap({
|
|
||||||
fontSize: "12px",
|
|
||||||
fontWeight: "400",
|
|
||||||
lineHeight: "18px",
|
|
||||||
alignSelf: "flex-end",
|
|
||||||
maxWidth: "30%",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
overflow: "hidden",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
});
|
|
||||||
|
|
||||||
const ENTITY_ID_STYLE = styleMap({
|
|
||||||
fontFamily: "var(--code-font-family, monospace)",
|
|
||||||
fontSize: "11px",
|
|
||||||
});
|
|
||||||
|
|
||||||
@customElement("ha-entity-picker")
|
@customElement("ha-entity-picker")
|
||||||
export class HaEntityPicker extends LitElement {
|
export class HaEntityPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -92,6 +42,8 @@ export class HaEntityPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,381 +95,237 @@ export class HaEntityPicker extends LitElement {
|
|||||||
public excludeEntities?: string[];
|
public excludeEntities?: string[];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||||
|
|
||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
@query("#anchor") private _anchor?: HaMdListItem;
|
||||||
|
|
||||||
|
@query("#input") private _input?: HaEntityComboBox;
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
private _renderContent() {
|
||||||
|
const entityId = this.value || "";
|
||||||
|
|
||||||
public async open() {
|
if (!this.value) {
|
||||||
await this.updateComplete;
|
return html`
|
||||||
await this.comboBox?.open();
|
<span slot="headline" class="placeholder"
|
||||||
}
|
>${this.placeholder ??
|
||||||
|
this.hass.localize(
|
||||||
public async focus() {
|
"ui.components.entity.entity-picker.placeholder"
|
||||||
await this.updateComplete;
|
)}</span
|
||||||
await this.comboBox?.focus();
|
>
|
||||||
}
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
|
`;
|
||||||
private _initialItems = false;
|
|
||||||
|
|
||||||
private _items: EntityPickerItem[] = [];
|
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
this.hass.loadBackendTranslation("title");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
|
|
||||||
item,
|
|
||||||
{ index }
|
|
||||||
) => html`
|
|
||||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
|
||||||
${item.icon_path
|
|
||||||
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
|
|
||||||
: html`
|
|
||||||
<state-badge
|
|
||||||
slot="start"
|
|
||||||
.stateObj=${item}
|
|
||||||
.hass=${this.hass}
|
|
||||||
></state-badge>
|
|
||||||
`}
|
|
||||||
|
|
||||||
<span slot="headline">${item.primary} </span>
|
|
||||||
${item.secondary
|
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
|
||||||
: nothing}
|
|
||||||
${item.entity_id && item.show_entity_id
|
|
||||||
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
|
|
||||||
>${item.entity_id}</span
|
|
||||||
>`
|
|
||||||
: nothing}
|
|
||||||
${item.translated_domain && !item.show_entity_id
|
|
||||||
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
|
|
||||||
${item.translated_domain}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
|
||||||
(
|
|
||||||
_opened: boolean,
|
|
||||||
hass: this["hass"],
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
|
||||||
includeEntities: this["includeEntities"],
|
|
||||||
excludeEntities: this["excludeEntities"],
|
|
||||||
createDomains: this["createDomains"]
|
|
||||||
): EntityPickerItem[] => {
|
|
||||||
let states: EntityPickerItem[] = [];
|
|
||||||
|
|
||||||
if (!hass) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let entityIds = Object.keys(hass.states);
|
|
||||||
|
|
||||||
const createItems = createDomains?.length
|
|
||||||
? createDomains.map((domain) => {
|
|
||||||
const primary = hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.create_helper",
|
|
||||||
{
|
|
||||||
domain: isHelperDomain(domain)
|
|
||||||
? hass.localize(
|
|
||||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
|
||||||
)
|
|
||||||
: domainToName(hass.localize, domain),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...FAKE_ENTITY,
|
|
||||||
entity_id: CREATE_ID + domain,
|
|
||||||
primary: primary,
|
|
||||||
label: primary,
|
|
||||||
secondary: this.hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.new_entity"
|
|
||||||
),
|
|
||||||
icon_path: mdiPlus,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!entityIds.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
...FAKE_ENTITY,
|
|
||||||
primary: this.hass!.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_entities"
|
|
||||||
),
|
|
||||||
label: this.hass!.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_entities"
|
|
||||||
),
|
|
||||||
icon_path: mdiMagnify,
|
|
||||||
},
|
|
||||||
...createItems,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeEntities) {
|
|
||||||
entityIds = entityIds.filter((entityId) =>
|
|
||||||
includeEntities.includes(entityId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeEntities) {
|
|
||||||
entityIds = entityIds.filter(
|
|
||||||
(entityId) => !excludeEntities.includes(entityId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeDomains) {
|
|
||||||
entityIds = entityIds.filter((eid) =>
|
|
||||||
includeDomains.includes(computeDomain(eid))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeDomains) {
|
|
||||||
entityIds = entityIds.filter(
|
|
||||||
(eid) => !excludeDomains.includes(computeDomain(eid))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = computeRTL(this.hass);
|
|
||||||
|
|
||||||
states = entityIds
|
|
||||||
.map<EntityPickerItem>((entityId) => {
|
|
||||||
const stateObj = hass!.states[entityId];
|
|
||||||
|
|
||||||
const { area, device } = getEntityContext(stateObj, hass);
|
|
||||||
|
|
||||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
|
||||||
const entityName = computeEntityName(stateObj, hass);
|
|
||||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
|
||||||
|
|
||||||
const primary = entityName || deviceName || entityId;
|
|
||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
|
||||||
|
|
||||||
const translatedDomain = domainToName(
|
|
||||||
this.hass.localize,
|
|
||||||
computeDomain(entityId)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...hass!.states[entityId],
|
|
||||||
primary: primary,
|
|
||||||
secondary:
|
|
||||||
secondary ||
|
|
||||||
this.hass.localize("ui.components.device-picker.no_area"),
|
|
||||||
label: friendlyName,
|
|
||||||
translated_domain: translatedDomain,
|
|
||||||
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
|
|
||||||
entity_name: entityName || deviceName,
|
|
||||||
area_name: areaName,
|
|
||||||
device_name: deviceName,
|
|
||||||
friendly_name: friendlyName,
|
|
||||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((entityA, entityB) =>
|
|
||||||
caseInsensitiveStringCompare(
|
|
||||||
entityA.sorting_label!,
|
|
||||||
entityB.sorting_label!,
|
|
||||||
this.hass.locale.language
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (includeDeviceClasses) {
|
|
||||||
states = states.filter(
|
|
||||||
(stateObj) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
stateObj.entity_id === this.value ||
|
|
||||||
(stateObj.attributes.device_class &&
|
|
||||||
includeDeviceClasses.includes(stateObj.attributes.device_class))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeUnitOfMeasurement) {
|
|
||||||
states = states.filter(
|
|
||||||
(stateObj) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
stateObj.entity_id === this.value ||
|
|
||||||
(stateObj.attributes.unit_of_measurement &&
|
|
||||||
includeUnitOfMeasurement.includes(
|
|
||||||
stateObj.attributes.unit_of_measurement
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entityFilter) {
|
|
||||||
states = states.filter(
|
|
||||||
(stateObj) =>
|
|
||||||
// We always want to include the entity of the current value
|
|
||||||
stateObj.entity_id === this.value || entityFilter!(stateObj)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!states.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
...FAKE_ENTITY,
|
|
||||||
primary: this.hass!.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_match"
|
|
||||||
),
|
|
||||||
label: this.hass!.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_match"
|
|
||||||
),
|
|
||||||
icon_path: mdiMagnify,
|
|
||||||
},
|
|
||||||
...createItems,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createItems?.length) {
|
|
||||||
states.push(...createItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
return states;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
changedProps.has("value") ||
|
|
||||||
changedProps.has("label") ||
|
|
||||||
changedProps.has("disabled")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !(!changedProps.has("_opened") && this._opened);
|
|
||||||
}
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
|
||||||
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
|
|
||||||
this._items = this._getItems(
|
|
||||||
this._opened,
|
|
||||||
this.hass,
|
|
||||||
this.includeDomains,
|
|
||||||
this.excludeDomains,
|
|
||||||
this.entityFilter,
|
|
||||||
this.includeDeviceClasses,
|
|
||||||
this.includeUnitOfMeasurement,
|
|
||||||
this.includeEntities,
|
|
||||||
this.excludeEntities,
|
|
||||||
this.createDomains
|
|
||||||
);
|
|
||||||
if (this._initialItems) {
|
|
||||||
this.comboBox.filteredItems = this._items;
|
|
||||||
}
|
|
||||||
this._initialItems = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
const stateObj = this.hass.states[entityId];
|
||||||
this.hass.loadFragmentTranslation("config");
|
|
||||||
}
|
const showClearIcon =
|
||||||
}
|
!this.required && !this.disabled && !this.hideClearIcon;
|
||||||
|
|
||||||
|
if (!stateObj) {
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||||
|
<span slot="headline">${entityId}</span>
|
||||||
|
${showClearIcon
|
||||||
|
? html`<ha-icon-button
|
||||||
|
class="clear"
|
||||||
|
slot="end"
|
||||||
|
@click=${this._clear}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||||
|
|
||||||
|
const entityName = computeEntityName(stateObj, this.hass);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || entityId;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<state-badge
|
||||||
item-value-path="entity_id"
|
|
||||||
.itemLabelPath=${this.itemLabelPath}
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._value}
|
.stateObj=${stateObj}
|
||||||
.label=${this.label === undefined
|
slot="start"
|
||||||
? this.hass.localize("ui.components.entity.entity-picker.entity")
|
></state-badge>
|
||||||
: this.label}
|
<span slot="headline">${primary}</span>
|
||||||
.helper=${this.helper}
|
<span slot="supporting-text">${secondary}</span>
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
${showClearIcon
|
||||||
.filteredItems=${this._items}
|
? html`<ha-icon-button
|
||||||
.renderer=${this._rowRenderer}
|
class="clear"
|
||||||
.required=${this.required}
|
slot="end"
|
||||||
.disabled=${this.disabled}
|
@click=${this._clear}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.path=${mdiClose}
|
||||||
@opened-changed=${this._openedChanged}
|
></ha-icon-button>`
|
||||||
@value-changed=${this._valueChanged}
|
: nothing}
|
||||||
@filter-changed=${this._filterChanged}
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
>
|
|
||||||
</ha-combo-box>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
protected render() {
|
||||||
return this.value || "";
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
|
<div class="container">
|
||||||
|
${!this._opened
|
||||||
|
? html`<ha-combo-box-item
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
id="anchor"
|
||||||
|
type="button"
|
||||||
|
compact
|
||||||
|
@click=${this._showPicker}
|
||||||
|
>
|
||||||
|
${this._renderContent()}
|
||||||
|
</ha-combo-box-item>`
|
||||||
|
: html`<ha-entity-combo-box
|
||||||
|
id="input"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.allowCustomEntity=${this.allowCustomEntity}
|
||||||
|
.label=${this.hass.localize("ui.common.search")}
|
||||||
|
.value=${this.value}
|
||||||
|
.createDomains=${this.createDomains}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDomains=${this.excludeDomains}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||||
|
.includeEntities=${this.includeEntities}
|
||||||
|
.excludeEntities=${this.excludeEntities}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
hide-clear-icon
|
||||||
|
@opened-changed=${this._debounceOpenedChanged}
|
||||||
|
@input=${stopPropagation}
|
||||||
|
></ha-entity-combo-box>`}
|
||||||
|
${this._renderHelper()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
private _renderHelper() {
|
||||||
this._opened = ev.detail.value;
|
return this.helper
|
||||||
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
|
: nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
private _clear(e) {
|
||||||
ev.stopPropagation();
|
e.stopPropagation();
|
||||||
const newValue = ev.detail.value?.trim();
|
this.value = undefined;
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
|
||||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
private async _showPicker() {
|
||||||
const domain = newValue.substring(CREATE_ID.length);
|
if (this.disabled) {
|
||||||
showHelperDetailDialog(this, {
|
|
||||||
domain,
|
|
||||||
dialogClosedCallback: (item) => {
|
|
||||||
if (item.entityId) this._setValue(item.entityId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._opened = true;
|
||||||
if (newValue !== this._value) {
|
await this.updateComplete;
|
||||||
this._setValue(newValue);
|
this._input?.focus();
|
||||||
}
|
this._input?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
|
// Multiple calls to _openedChanged can be triggered in quick succession
|
||||||
Fuse.createIndex(
|
// when the menu is opened
|
||||||
[
|
private _debounceOpenedChanged = debounce(
|
||||||
"entity_name",
|
(ev) => this._openedChanged(ev),
|
||||||
"device_name",
|
10
|
||||||
"area_name",
|
|
||||||
"translated_domain",
|
|
||||||
"friendly_name", // for backwards compatibility
|
|
||||||
"entity_id", // for technical search
|
|
||||||
],
|
|
||||||
states
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||||
if (!this._opened) return;
|
const opened = ev.detail.value;
|
||||||
|
if (this._opened && !opened) {
|
||||||
const target = ev.target as HaComboBox;
|
this._opened = false;
|
||||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
await this.updateComplete;
|
||||||
|
this._anchor?.focus();
|
||||||
const index = this._fuseIndex(this._items);
|
|
||||||
const fuse = new HaFuse(this._items, {}, index);
|
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(filterString);
|
|
||||||
if (results) {
|
|
||||||
target.filteredItems = results.map((result) => result.item);
|
|
||||||
} else {
|
|
||||||
target.filteredItems = this._items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string | undefined) {
|
static get styles(): CSSResultGroup {
|
||||||
this.value = value;
|
return [
|
||||||
setTimeout(() => {
|
css`
|
||||||
fireEvent(this, "value-changed", { value });
|
mwc-menu-surface {
|
||||||
fireEvent(this, "change");
|
--mdc-menu-min-width: 100%;
|
||||||
}, 0);
|
}
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-combo-box-item {
|
||||||
|
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
--md-list-item-one-line-container-height: 56px;
|
||||||
|
--md-list-item-two-line-container-height: 56px;
|
||||||
|
--md-list-item-top-space: 8px;
|
||||||
|
--md-list-item-bottom-space: 8px;
|
||||||
|
--md-list-item-leading-space: 8px;
|
||||||
|
--md-list-item-trailing-space: 8px;
|
||||||
|
--ha-md-list-item-gap: 8px;
|
||||||
|
/* Remove the default focus ring */
|
||||||
|
--md-focus-ring-width: 0px;
|
||||||
|
--md-focus-ring-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Similar focus style as the text field */
|
||||||
|
ha-combo-box-item:after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(
|
||||||
|
--mdc-text-field-idle-line-color,
|
||||||
|
rgba(0, 0, 0, 0.42)
|
||||||
|
);
|
||||||
|
transform:
|
||||||
|
height 180ms ease-in-out,
|
||||||
|
background-color 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-combo-box-item:focus:after {
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--mdc-theme-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-combo-box-item ha-svg-icon[slot="start"] {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
margin: 0 -8px;
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
.edit {
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
481
src/components/entity/ha-statistic-combo-box.ts
Normal file
481
src/components/entity/ha-statistic-combo-box.ts
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
|
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||||
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import type { StatisticsMetaData } from "../../data/recorder";
|
||||||
|
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
|
||||||
|
import { HaFuse } from "../../resources/fuse";
|
||||||
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
|
import "../ha-combo-box";
|
||||||
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
|
import "../ha-combo-box-item";
|
||||||
|
import "../ha-svg-icon";
|
||||||
|
import "./state-badge";
|
||||||
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
|
|
||||||
|
type StatisticItemType = "entity" | "external" | "no_state";
|
||||||
|
|
||||||
|
interface StatisticItem {
|
||||||
|
// Force empty label to always display empty value by default in the search field
|
||||||
|
id: string;
|
||||||
|
statistic_id?: string;
|
||||||
|
label: "";
|
||||||
|
primary: string;
|
||||||
|
secondary?: string;
|
||||||
|
search_labels?: string[];
|
||||||
|
sorting_label?: string;
|
||||||
|
icon_path?: string;
|
||||||
|
type?: StatisticItemType;
|
||||||
|
stateObj?: HassEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MISSING_ID = "___missing-entity___";
|
||||||
|
|
||||||
|
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
||||||
|
|
||||||
|
@customElement("ha-statistic-combo-box")
|
||||||
|
export class HaStatisticComboBox extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "statistic-types" })
|
||||||
|
public statisticTypes?: "mean" | "sum";
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
|
public allowCustomEntity;
|
||||||
|
|
||||||
|
@property({ attribute: false, type: Array })
|
||||||
|
public statisticIds?: StatisticsMetaData[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only statistics natively stored with these units of measurements.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-statistics-unit-of-measurement
|
||||||
|
*/
|
||||||
|
@property({
|
||||||
|
type: Array,
|
||||||
|
attribute: "include-statistics-unit-of-measurement",
|
||||||
|
})
|
||||||
|
public includeStatisticsUnitOfMeasurement?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only statistics with these unit classes.
|
||||||
|
* @attr include-unit-class
|
||||||
|
*/
|
||||||
|
@property({ attribute: "include-unit-class" })
|
||||||
|
public includeUnitClass?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only statistics with these device classes.
|
||||||
|
* @attr include-device-class
|
||||||
|
*/
|
||||||
|
@property({ attribute: "include-device-class" })
|
||||||
|
public includeDeviceClass?: string | string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only statistics on entities.
|
||||||
|
* @type {Boolean}
|
||||||
|
* @attr entities-only
|
||||||
|
*/
|
||||||
|
@property({ type: Boolean, attribute: "entities-only" })
|
||||||
|
public entitiesOnly = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of statistics to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-statistics
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-statistics" })
|
||||||
|
public excludeStatistics?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||||
|
"/more-info/statistics/";
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _initialItems = false;
|
||||||
|
|
||||||
|
private _items: StatisticItem[] = [];
|
||||||
|
|
||||||
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
this.hass.loadBackendTranslation("title");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
|
||||||
|
item,
|
||||||
|
{ index }
|
||||||
|
) => {
|
||||||
|
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||||
|
return html`
|
||||||
|
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||||
|
${item.icon_path
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
style="margin: 0 4px"
|
||||||
|
slot="start"
|
||||||
|
.path=${item.icon_path}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: item.stateObj
|
||||||
|
? html`
|
||||||
|
<state-badge
|
||||||
|
slot="start"
|
||||||
|
.stateObj=${item.stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></state-badge>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<span slot="headline">${item.primary} </span>
|
||||||
|
${item.secondary
|
||||||
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
|
: nothing}
|
||||||
|
${item.id && showEntityId
|
||||||
|
? html`<span slot="supporting-text" class="code">
|
||||||
|
${item.statistic_id}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getItems = memoizeOne(
|
||||||
|
(
|
||||||
|
_opened: boolean,
|
||||||
|
hass: this["hass"],
|
||||||
|
statisticIds: StatisticsMetaData[],
|
||||||
|
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||||
|
includeUnitClass?: string | string[],
|
||||||
|
includeDeviceClass?: string | string[],
|
||||||
|
entitiesOnly?: boolean,
|
||||||
|
excludeStatistics?: string[],
|
||||||
|
value?: string
|
||||||
|
): StatisticItem[] => {
|
||||||
|
if (!statisticIds.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
label: "",
|
||||||
|
primary: this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.no_statistics"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeStatisticsUnitOfMeasurement) {
|
||||||
|
const includeUnits: (string | null)[] = ensureArray(
|
||||||
|
includeStatisticsUnitOfMeasurement
|
||||||
|
);
|
||||||
|
statisticIds = statisticIds.filter((meta) =>
|
||||||
|
includeUnits.includes(meta.statistics_unit_of_measurement)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (includeUnitClass) {
|
||||||
|
const includeUnitClasses: (string | null)[] =
|
||||||
|
ensureArray(includeUnitClass);
|
||||||
|
statisticIds = statisticIds.filter((meta) =>
|
||||||
|
includeUnitClasses.includes(meta.unit_class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (includeDeviceClass) {
|
||||||
|
const includeDeviceClasses: (string | null)[] =
|
||||||
|
ensureArray(includeDeviceClass);
|
||||||
|
statisticIds = statisticIds.filter((meta) => {
|
||||||
|
const stateObj = this.hass.states[meta.statistic_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return includeDeviceClasses.includes(
|
||||||
|
stateObj.attributes.device_class || ""
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
|
const output: StatisticItem[] = [];
|
||||||
|
statisticIds.forEach((meta) => {
|
||||||
|
if (
|
||||||
|
excludeStatistics &&
|
||||||
|
meta.statistic_id !== value &&
|
||||||
|
excludeStatistics.includes(meta.statistic_id)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stateObj = this.hass.states[meta.statistic_id];
|
||||||
|
|
||||||
|
if (!stateObj) {
|
||||||
|
if (!entitiesOnly) {
|
||||||
|
const id = meta.statistic_id;
|
||||||
|
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
|
||||||
|
const type =
|
||||||
|
meta.statistic_id.includes(":") &&
|
||||||
|
!meta.statistic_id.includes(".")
|
||||||
|
? "external"
|
||||||
|
: "no_state";
|
||||||
|
|
||||||
|
if (type === "no_state") {
|
||||||
|
output.push({
|
||||||
|
id,
|
||||||
|
primary: label,
|
||||||
|
secondary: this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.no_state"
|
||||||
|
),
|
||||||
|
label: "",
|
||||||
|
type,
|
||||||
|
sorting_label: label,
|
||||||
|
search_labels: [label, id],
|
||||||
|
icon_path: mdiShape,
|
||||||
|
});
|
||||||
|
} else if (type === "external") {
|
||||||
|
const domain = id.split(":")[0];
|
||||||
|
const domainName = domainToName(this.hass.localize, domain);
|
||||||
|
output.push({
|
||||||
|
id,
|
||||||
|
statistic_id: id,
|
||||||
|
primary: label,
|
||||||
|
secondary: domainName,
|
||||||
|
label: "",
|
||||||
|
type,
|
||||||
|
sorting_label: label,
|
||||||
|
search_labels: [label, domainName, id],
|
||||||
|
icon_path: mdiChartLine,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = meta.statistic_id;
|
||||||
|
|
||||||
|
const { area, device } = getEntityContext(stateObj, hass);
|
||||||
|
|
||||||
|
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||||
|
const entityName = computeEntityName(stateObj, hass);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || id;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
id,
|
||||||
|
statistic_id: id,
|
||||||
|
label: "",
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
stateObj: stateObj,
|
||||||
|
type: "entity",
|
||||||
|
sorting_label: [deviceName, entityName].join("_"),
|
||||||
|
search_labels: [
|
||||||
|
entityName,
|
||||||
|
deviceName,
|
||||||
|
areaName,
|
||||||
|
friendlyName,
|
||||||
|
id,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!output.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
primary: this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.no_match"
|
||||||
|
),
|
||||||
|
label: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.length > 1) {
|
||||||
|
output.sort((a, b) => {
|
||||||
|
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
|
||||||
|
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
|
||||||
|
|
||||||
|
return caseInsensitiveStringCompare(
|
||||||
|
`${aPrefix}_${a.sorting_label || ""}`,
|
||||||
|
`${bPrefix}_${b.sorting_label || ""}`,
|
||||||
|
this.hass.locale.language
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
id: MISSING_ID,
|
||||||
|
primary: this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.missing_entity"
|
||||||
|
),
|
||||||
|
label: "",
|
||||||
|
icon_path: mdiHelpCircle,
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
changedProps.has("value") ||
|
||||||
|
changedProps.has("label") ||
|
||||||
|
changedProps.has("disabled")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !(!changedProps.has("_opened") && this._opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this.hasUpdated && !this.statisticIds) ||
|
||||||
|
changedProps.has("statisticTypes")
|
||||||
|
) {
|
||||||
|
this._getStatisticIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.statisticIds &&
|
||||||
|
(!this._initialItems || (changedProps.has("_opened") && this._opened))
|
||||||
|
) {
|
||||||
|
this._items = this._getItems(
|
||||||
|
this._opened,
|
||||||
|
this.hass,
|
||||||
|
this.statisticIds!,
|
||||||
|
this.includeStatisticsUnitOfMeasurement,
|
||||||
|
this.includeUnitClass,
|
||||||
|
this.includeDeviceClass,
|
||||||
|
this.entitiesOnly,
|
||||||
|
this.excludeStatistics,
|
||||||
|
this.value
|
||||||
|
);
|
||||||
|
if (this._initialItems) {
|
||||||
|
this.comboBox.filteredItems = this._items;
|
||||||
|
}
|
||||||
|
this._initialItems = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | typeof nothing {
|
||||||
|
if (this._items.length === 0) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.statistic-picker.statistic")
|
||||||
|
: this.label}
|
||||||
|
.value=${this._value}
|
||||||
|
.renderer=${this._rowRenderer}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
|
.filteredItems=${this._items}
|
||||||
|
item-value-path="id"
|
||||||
|
item-id-path="id"
|
||||||
|
item-label-path="label"
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._statisticChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
></ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getStatisticIds() {
|
||||||
|
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _statisticChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
let newValue = ev.detail.value;
|
||||||
|
if (newValue === MISSING_ID) {
|
||||||
|
newValue = "";
|
||||||
|
window.open(
|
||||||
|
documentationUrl(this.hass, this.helpMissingEntityUrl),
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
|
||||||
|
Fuse.createIndex(["search_labels"], states)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
if (!this._opened) return;
|
||||||
|
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
||||||
|
|
||||||
|
const index = this._fuseIndex(this._items);
|
||||||
|
const fuse = new HaFuse(this._items, {}, index);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(filterString);
|
||||||
|
|
||||||
|
if (results) {
|
||||||
|
target.filteredItems = results.map((result) => result.item);
|
||||||
|
} else {
|
||||||
|
target.filteredItems = this._items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-statistic-combo-box": HaStatisticComboBox;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,65 +1,66 @@
|
|||||||
import { mdiChartLine } from "@mdi/js";
|
import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues, TemplateResult } from "lit";
|
import {
|
||||||
import { html, LitElement, nothing } from "lit";
|
css,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
type CSSResultGroup,
|
||||||
|
type PropertyValues,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import { getEntityContext } from "../../common/entity/get_entity_context";
|
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import type { StatisticsMetaData } from "../../data/recorder";
|
import {
|
||||||
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
|
getStatisticIds,
|
||||||
import { HaFuse } from "../../resources/fuse";
|
getStatisticLabel,
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
type StatisticsMetaData,
|
||||||
import "../ha-combo-box";
|
} from "../../data/recorder";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
|
import "../ha-icon-button";
|
||||||
|
import type { HaMdListItem } from "../ha-md-list-item";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
|
import "./ha-entity-combo-box";
|
||||||
|
import type { HaEntityComboBox } from "./ha-entity-combo-box";
|
||||||
|
import "./ha-statistic-combo-box";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
|
|
||||||
type StatisticItemType = "entity" | "external" | "no_state";
|
|
||||||
|
|
||||||
interface StatisticItem {
|
interface StatisticItem {
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
primary: string;
|
primary: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
show_entity_id?: boolean;
|
|
||||||
entity_name?: string;
|
|
||||||
area_name?: string;
|
|
||||||
device_name?: string;
|
|
||||||
friendly_name?: string;
|
|
||||||
sorting_label?: string;
|
|
||||||
state?: HassEntity;
|
|
||||||
type?: StatisticItemType;
|
|
||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
|
stateObj?: HassEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
|
||||||
|
|
||||||
const ENTITY_ID_STYLE = styleMap({
|
|
||||||
fontFamily: "var(--code-font-family, monospace)",
|
|
||||||
fontSize: "11px",
|
|
||||||
});
|
|
||||||
|
|
||||||
@customElement("ha-statistic-picker")
|
@customElement("ha-statistic-picker")
|
||||||
export class HaStatisticPicker extends LitElement {
|
export class HaStatisticPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ attribute: "statistic-types" })
|
@property({ attribute: "statistic-types" })
|
||||||
public statisticTypes?: "mean" | "sum";
|
public statisticTypes?: "mean" | "sum";
|
||||||
|
|
||||||
@@ -69,8 +70,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public statisticIds?: StatisticsMetaData[];
|
public statisticIds?: StatisticsMetaData[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show only statistics natively stored with these units of measurements.
|
* Show only statistics natively stored with these units of measurements.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -112,251 +111,15 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ type: Array, attribute: "exclude-statistics" })
|
@property({ type: Array, attribute: "exclude-statistics" })
|
||||||
public excludeStatistics?: string[];
|
public excludeStatistics?: string[];
|
||||||
|
|
||||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
"/more-info/statistics/";
|
public hideClearIcon = false;
|
||||||
|
|
||||||
|
@query("#anchor") private _anchor?: HaMdListItem;
|
||||||
|
|
||||||
|
@query("#input") private _input?: HaEntityComboBox;
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
|
||||||
|
|
||||||
private _initialItems = false;
|
|
||||||
|
|
||||||
private _items: StatisticItem[] = [];
|
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
this.hass.loadBackendTranslation("title");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
|
|
||||||
item,
|
|
||||||
{ index }
|
|
||||||
) => html`
|
|
||||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
|
||||||
${!item.state
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
style="margin: 0 4px"
|
|
||||||
slot="start"
|
|
||||||
.path=${item.iconPath}
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: html`
|
|
||||||
<state-badge
|
|
||||||
slot="start"
|
|
||||||
.stateObj=${item.state}
|
|
||||||
.hass=${this.hass}
|
|
||||||
></state-badge>
|
|
||||||
`}
|
|
||||||
|
|
||||||
<span slot="headline">${item.primary} </span>
|
|
||||||
${item.secondary
|
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
|
||||||
: nothing}
|
|
||||||
${item.id && item.show_entity_id
|
|
||||||
? html`
|
|
||||||
<span slot="supporting-text" style=${ENTITY_ID_STYLE}>
|
|
||||||
${item.id}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
|
||||||
(
|
|
||||||
_opened: boolean,
|
|
||||||
hass: this["hass"],
|
|
||||||
statisticIds: StatisticsMetaData[],
|
|
||||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
|
||||||
includeUnitClass?: string | string[],
|
|
||||||
includeDeviceClass?: string | string[],
|
|
||||||
entitiesOnly?: boolean,
|
|
||||||
excludeStatistics?: string[],
|
|
||||||
value?: string
|
|
||||||
): StatisticItem[] => {
|
|
||||||
if (!statisticIds.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
label: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_statistics"
|
|
||||||
),
|
|
||||||
primary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_statistics"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeStatisticsUnitOfMeasurement) {
|
|
||||||
const includeUnits: (string | null)[] = ensureArray(
|
|
||||||
includeStatisticsUnitOfMeasurement
|
|
||||||
);
|
|
||||||
statisticIds = statisticIds.filter((meta) =>
|
|
||||||
includeUnits.includes(meta.statistics_unit_of_measurement)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (includeUnitClass) {
|
|
||||||
const includeUnitClasses: (string | null)[] =
|
|
||||||
ensureArray(includeUnitClass);
|
|
||||||
statisticIds = statisticIds.filter((meta) =>
|
|
||||||
includeUnitClasses.includes(meta.unit_class)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (includeDeviceClass) {
|
|
||||||
const includeDeviceClasses: (string | null)[] =
|
|
||||||
ensureArray(includeDeviceClass);
|
|
||||||
statisticIds = statisticIds.filter((meta) => {
|
|
||||||
const stateObj = this.hass.states[meta.statistic_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return includeDeviceClasses.includes(
|
|
||||||
stateObj.attributes.device_class || ""
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRTL = computeRTL(this.hass);
|
|
||||||
|
|
||||||
const output: StatisticItem[] = [];
|
|
||||||
statisticIds.forEach((meta) => {
|
|
||||||
if (
|
|
||||||
excludeStatistics &&
|
|
||||||
meta.statistic_id !== value &&
|
|
||||||
excludeStatistics.includes(meta.statistic_id)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const entityState = this.hass.states[meta.statistic_id];
|
|
||||||
if (!entityState) {
|
|
||||||
if (!entitiesOnly) {
|
|
||||||
const id = meta.statistic_id;
|
|
||||||
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
|
|
||||||
const type =
|
|
||||||
meta.statistic_id.includes(":") &&
|
|
||||||
!meta.statistic_id.includes(".")
|
|
||||||
? "external"
|
|
||||||
: "no_state";
|
|
||||||
|
|
||||||
if (type === "no_state") {
|
|
||||||
output.push({
|
|
||||||
id,
|
|
||||||
primary: label,
|
|
||||||
secondary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_state"
|
|
||||||
),
|
|
||||||
label,
|
|
||||||
type,
|
|
||||||
sorting_label: label,
|
|
||||||
});
|
|
||||||
} else if (type === "external") {
|
|
||||||
const domain = id.split(":")[0];
|
|
||||||
const domainName = domainToName(this.hass.localize, domain);
|
|
||||||
output.push({
|
|
||||||
id,
|
|
||||||
primary: label,
|
|
||||||
secondary: domainName,
|
|
||||||
label,
|
|
||||||
type,
|
|
||||||
sorting_label: label,
|
|
||||||
iconPath: mdiChartLine,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = meta.statistic_id;
|
|
||||||
|
|
||||||
const { area, device } = getEntityContext(entityState, hass);
|
|
||||||
|
|
||||||
const friendlyName = computeStateName(entityState); // Keep this for search
|
|
||||||
const entityName = computeEntityName(entityState, hass);
|
|
||||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
|
||||||
|
|
||||||
const primary = entityName || deviceName || id;
|
|
||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
|
||||||
|
|
||||||
output.push({
|
|
||||||
id,
|
|
||||||
primary,
|
|
||||||
secondary,
|
|
||||||
label: friendlyName,
|
|
||||||
state: entityState,
|
|
||||||
type: "entity",
|
|
||||||
sorting_label: [deviceName, entityName].join("_"),
|
|
||||||
entity_name: entityName || deviceName,
|
|
||||||
area_name: areaName,
|
|
||||||
device_name: deviceName,
|
|
||||||
friendly_name: friendlyName,
|
|
||||||
show_entity_id: hass.userData?.showEntityIdPicker,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!output.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
primary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_match"
|
|
||||||
),
|
|
||||||
label: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_match"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.length > 1) {
|
|
||||||
output.sort((a, b) => {
|
|
||||||
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
|
|
||||||
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
|
|
||||||
|
|
||||||
return caseInsensitiveStringCompare(
|
|
||||||
`${aPrefix}_${a.sorting_label || ""}`,
|
|
||||||
`${bPrefix}_${b.sorting_label || ""}`,
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push({
|
|
||||||
id: "__missing",
|
|
||||||
primary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.missing_entity"
|
|
||||||
),
|
|
||||||
label: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.missing_entity"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public async open() {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this.comboBox?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async focus() {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this.comboBox?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
changedProps.has("value") ||
|
|
||||||
changedProps.has("label") ||
|
|
||||||
changedProps.has("disabled")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !(!changedProps.has("_opened") && this._opened);
|
|
||||||
}
|
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
if (
|
if (
|
||||||
(!this.hasUpdated && !this.statisticIds) ||
|
(!this.hasUpdated && !this.statisticIds) ||
|
||||||
@@ -364,117 +127,278 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
) {
|
) {
|
||||||
this._getStatisticIds();
|
this._getStatisticIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
this.statisticIds &&
|
|
||||||
(!this._initialItems || (changedProps.has("_opened") && this._opened))
|
|
||||||
) {
|
|
||||||
this._items = this._getItems(
|
|
||||||
this._opened,
|
|
||||||
this.hass,
|
|
||||||
this.statisticIds!,
|
|
||||||
this.includeStatisticsUnitOfMeasurement,
|
|
||||||
this.includeUnitClass,
|
|
||||||
this.includeDeviceClass,
|
|
||||||
this.entitiesOnly,
|
|
||||||
this.excludeStatistics,
|
|
||||||
this.value
|
|
||||||
);
|
|
||||||
if (this._initialItems) {
|
|
||||||
this.comboBox.filteredItems = this._items;
|
|
||||||
}
|
|
||||||
this._initialItems = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult | typeof nothing {
|
|
||||||
if (this._items.length === 0) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-combo-box
|
|
||||||
.hass=${this.hass}
|
|
||||||
.label=${this.label === undefined && this.hass
|
|
||||||
? this.hass.localize("ui.components.statistic-picker.statistic")
|
|
||||||
: this.label}
|
|
||||||
.value=${this._value}
|
|
||||||
.renderer=${this._rowRenderer}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
|
||||||
.filteredItems=${this._items}
|
|
||||||
item-value-path="id"
|
|
||||||
item-id-path="id"
|
|
||||||
item-label-path="label"
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@value-changed=${this._statisticChanged}
|
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
></ha-combo-box>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _getStatisticIds() {
|
private async _getStatisticIds() {
|
||||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private _statisticMetaData = memoizeOne(
|
||||||
return this.value || "";
|
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
|
||||||
}
|
if (!statisticIds) {
|
||||||
|
return undefined;
|
||||||
private _statisticChanged(ev: ValueChangedEvent<string>) {
|
}
|
||||||
ev.stopPropagation();
|
return statisticIds.find(
|
||||||
let newValue = ev.detail.value;
|
(statistic) => statistic.statistic_id === statisticId
|
||||||
if (newValue === "__missing") {
|
);
|
||||||
newValue = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
|
||||||
this._setValue(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
|
|
||||||
Fuse.createIndex(
|
|
||||||
[
|
|
||||||
"label",
|
|
||||||
"entity_name",
|
|
||||||
"device_name",
|
|
||||||
"area_name",
|
|
||||||
"friendly_name", // for backwards compatibility
|
|
||||||
"id", // for technical search
|
|
||||||
],
|
|
||||||
states
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private _renderContent() {
|
||||||
if (!this._opened) return;
|
const statisticId = this.value || "";
|
||||||
|
|
||||||
const target = ev.target as HaComboBox;
|
if (!this.value) {
|
||||||
const filterString = ev.detail.value.trim().toLowerCase() as string;
|
return html`
|
||||||
|
<span slot="headline" class="placeholder"
|
||||||
|
>${this.placeholder ??
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.placeholder"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const index = this._fuseIndex(this._items);
|
const item = this._computeItem(statisticId);
|
||||||
const fuse = new HaFuse(this._items, {}, index);
|
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(filterString);
|
const showClearIcon =
|
||||||
|
!this.required && !this.disabled && !this.hideClearIcon;
|
||||||
|
|
||||||
if (results) {
|
return html`
|
||||||
target.filteredItems = results.map((result) => result.item);
|
${item.stateObj
|
||||||
} else {
|
? html`
|
||||||
target.filteredItems = this._items;
|
<state-badge
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${item.stateObj}
|
||||||
|
slot="start"
|
||||||
|
></state-badge>
|
||||||
|
`
|
||||||
|
: item.iconPath
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${item.iconPath}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
${item.secondary
|
||||||
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
|
: nothing}
|
||||||
|
${showClearIcon
|
||||||
|
? html`<ha-icon-button
|
||||||
|
class="clear"
|
||||||
|
slot="end"
|
||||||
|
@click=${this._clear}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeItem(statisticId: string): StatisticItem {
|
||||||
|
const stateObj = this.hass.states[statisticId];
|
||||||
|
|
||||||
|
if (stateObj) {
|
||||||
|
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||||
|
|
||||||
|
const entityName = computeEntityName(stateObj, this.hass);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || statisticId;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
stateObj,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const statistic = this.statisticIds
|
||||||
|
? this._statisticMetaData(statisticId, this.statisticIds)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (statistic) {
|
||||||
|
const type =
|
||||||
|
statisticId.includes(":") && !statisticId.includes(".")
|
||||||
|
? "external"
|
||||||
|
: "no_state";
|
||||||
|
|
||||||
|
if (type === "external") {
|
||||||
|
const label = getStatisticLabel(this.hass, statisticId, statistic);
|
||||||
|
const domain = statisticId.split(":")[0];
|
||||||
|
const domainName = domainToName(this.hass.localize, domain);
|
||||||
|
|
||||||
|
return {
|
||||||
|
primary: label,
|
||||||
|
secondary: domainName,
|
||||||
|
iconPath: mdiChartLine,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
primary: statisticId,
|
||||||
|
iconPath: mdiShape,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
|
<div class="container">
|
||||||
|
${!this._opened
|
||||||
|
? html`
|
||||||
|
<ha-combo-box-item
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
id="anchor"
|
||||||
|
type="button"
|
||||||
|
compact
|
||||||
|
@click=${this._showPicker}
|
||||||
|
>
|
||||||
|
${this._renderContent()}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-statistic-combo-box
|
||||||
|
id="input"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.allowCustomEntity=${this.allowCustomEntity}
|
||||||
|
.label=${this.hass.localize("ui.common.search")}
|
||||||
|
.value=${this.value}
|
||||||
|
.includeStatisticsUnitOfMeasurement=${this
|
||||||
|
.includeStatisticsUnitOfMeasurement}
|
||||||
|
.includeUnitClass=${this.includeUnitClass}
|
||||||
|
.includeDeviceClass=${this.includeDeviceClass}
|
||||||
|
.statisticTypes=${this.statisticTypes}
|
||||||
|
.statisticIds=${this.statisticIds}
|
||||||
|
.excludeStatistics=${this.excludeStatistics}
|
||||||
|
hide-clear-icon
|
||||||
|
@opened-changed=${this._debounceOpenedChanged}
|
||||||
|
@input=${stopPropagation}
|
||||||
|
></ha-statistic-combo-box>
|
||||||
|
`}
|
||||||
|
${this._renderHelper()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderHelper() {
|
||||||
|
return this.helper
|
||||||
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clear(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _showPicker() {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._opened = true;
|
||||||
|
await this.updateComplete;
|
||||||
|
this._input?.focus();
|
||||||
|
this._input?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple calls to _openedChanged can be triggered in quick succession
|
||||||
|
// when the menu is opened
|
||||||
|
private _debounceOpenedChanged = debounce(
|
||||||
|
(ev) => this._openedChanged(ev),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||||
|
const opened = ev.detail.value;
|
||||||
|
if (this._opened && !opened) {
|
||||||
|
this._opened = false;
|
||||||
|
await this.updateComplete;
|
||||||
|
this._anchor?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string) {
|
static get styles(): CSSResultGroup {
|
||||||
this.value = value;
|
return [
|
||||||
setTimeout(() => {
|
css`
|
||||||
fireEvent(this, "value-changed", { value });
|
.container {
|
||||||
fireEvent(this, "change");
|
position: relative;
|
||||||
}, 0);
|
display: block;
|
||||||
|
}
|
||||||
|
ha-combo-box-item {
|
||||||
|
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
--md-list-item-one-line-container-height: 56px;
|
||||||
|
--md-list-item-two-line-container-height: 56px;
|
||||||
|
--md-list-item-top-space: 8px;
|
||||||
|
--md-list-item-bottom-space: 8px;
|
||||||
|
--md-list-item-leading-space: 8px;
|
||||||
|
--md-list-item-trailing-space: 8px;
|
||||||
|
--ha-md-list-item-gap: 8px;
|
||||||
|
/* Remove the default focus ring */
|
||||||
|
--md-focus-ring-width: 0px;
|
||||||
|
--md-focus-ring-duration: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Similar focus style as the text field */
|
||||||
|
ha-combo-box-item:after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(
|
||||||
|
--mdc-text-field-idle-line-color,
|
||||||
|
rgba(0, 0, 0, 0.42)
|
||||||
|
);
|
||||||
|
transform:
|
||||||
|
height 180ms ease-in-out,
|
||||||
|
background-color 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-combo-box-item:focus:after {
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--mdc-theme-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-combo-box-item ha-svg-icon[slot="start"] {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
margin: 0 -8px;
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
.edit {
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
@property({ attribute: "statistic-types" })
|
@property({ attribute: "statistic-types" })
|
||||||
public statisticTypes?: "mean" | "sum";
|
public statisticTypes?: "mean" | "sum";
|
||||||
|
|
||||||
@property({ attribute: "picked-statistic-label" })
|
@property({ type: String })
|
||||||
public pickedStatisticLabel?: string;
|
public label?: string;
|
||||||
|
|
||||||
@property({ attribute: "pick-statistic-label" })
|
@property({ type: String })
|
||||||
public pickStatisticLabel?: string;
|
public placeholder?: string;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
public allowCustomEntity;
|
public allowCustomEntity;
|
||||||
@@ -82,6 +82,7 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
: this.statisticTypes;
|
: this.statisticTypes;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
${repeat(
|
${repeat(
|
||||||
this._currentStatistics,
|
this._currentStatistics,
|
||||||
(statisticId) => statisticId,
|
(statisticId) => statisticId,
|
||||||
@@ -96,7 +97,6 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
.value=${statisticId}
|
.value=${statisticId}
|
||||||
.statisticTypes=${includeStatisticTypesCurrent}
|
.statisticTypes=${includeStatisticTypesCurrent}
|
||||||
.statisticIds=${this.statisticIds}
|
.statisticIds=${this.statisticIds}
|
||||||
.label=${this.pickedStatisticLabel}
|
|
||||||
.excludeStatistics=${this.value}
|
.excludeStatistics=${this.value}
|
||||||
.allowCustomEntity=${this.allowCustomEntity}
|
.allowCustomEntity=${this.allowCustomEntity}
|
||||||
@value-changed=${this._statisticChanged}
|
@value-changed=${this._statisticChanged}
|
||||||
@@ -113,7 +113,7 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
.includeDeviceClass=${this.includeDeviceClass}
|
.includeDeviceClass=${this.includeDeviceClass}
|
||||||
.statisticTypes=${this.statisticTypes}
|
.statisticTypes=${this.statisticTypes}
|
||||||
.statisticIds=${this.statisticIds}
|
.statisticIds=${this.statisticIds}
|
||||||
.label=${this.pickStatisticLabel}
|
.placeholder=${this.placeholder}
|
||||||
.excludeStatistics=${this.value}
|
.excludeStatistics=${this.value}
|
||||||
.allowCustomEntity=${this.allowCustomEntity}
|
.allowCustomEntity=${this.allowCustomEntity}
|
||||||
@value-changed=${this._addStatistic}
|
@value-changed=${this._addStatistic}
|
||||||
@@ -181,6 +181,10 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0 0 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items: DisplayItem[] = areas.map((area) => {
|
const items: DisplayItem[] = areas.map((area) => {
|
||||||
const { floor } = getAreaContext(area.area_id, this.hass!);
|
const { floor } = getAreaContext(area, this.hass!);
|
||||||
return {
|
return {
|
||||||
value: area.area_id,
|
value: area.area_id,
|
||||||
label: area.name,
|
label: area.name,
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class HaBadge extends LitElement {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
font-size: var(--ha-font-size-badge, var(--ha-font-size-s));
|
font-size: var(--ha-badge-font-size, var(--ha-font-size-s));
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
@@ -111,7 +111,7 @@ export class HaBadge extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]) {
|
||||||
--mdc-icon-size: var(--ha-icon-size-badge, 18px);
|
--mdc-icon-size: var(--ha-badge-icon-size, 18px);
|
||||||
color: var(--badge-color);
|
color: var(--badge-color);
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export class HaComboBoxItem extends HaMdListItem {
|
|||||||
:host([border-top]) md-item {
|
:host([border-top]) md-item {
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
[slot="start"] {
|
||||||
|
--state-icon-color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
[slot="headline"] {
|
[slot="headline"] {
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -32,6 +35,20 @@ export class HaComboBoxItem extends HaMdListItem {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
::slotted(.code) {
|
||||||
|
font-family: var(--ha-font-family-code);
|
||||||
|
font-size: var(--ha-font-size-xs);
|
||||||
|
}
|
||||||
|
[slot="trailing-supporting-text"] {
|
||||||
|
font-size: var(--ha-font-size-s);
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
line-height: var(--ha-line-height-normal);
|
||||||
|
align-self: flex-end;
|
||||||
|
max-width: 30%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ export class HaComboBox extends LitElement {
|
|||||||
this._comboBox.value = value;
|
this._comboBox.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setTextFieldValue(value: string) {
|
||||||
|
this._inputElement.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ class HaLabeledSlider extends LitElement {
|
|||||||
@property({ type: Number }) public value?: number;
|
@property({ type: Number }) public value?: number;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
const title = this._getTitle();
|
||||||
return html`
|
return html`
|
||||||
<div class="title">${this._getTitle()}</div>
|
${title ? html`<div class="title">${title}</div>` : nothing}
|
||||||
<div class="extra-container"><slot name="extra"></slot></div>
|
<div class="extra-container"><slot name="extra"></slot></div>
|
||||||
<div class="slider-container">
|
<div class="slider-container">
|
||||||
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
|
${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing}
|
||||||
@@ -73,17 +74,20 @@ class HaLabeledSlider extends LitElement {
|
|||||||
|
|
||||||
.slider-container {
|
.slider-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-icon {
|
ha-icon {
|
||||||
margin-top: 8px;
|
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-slider {
|
ha-slider {
|
||||||
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
align-items: center;
|
||||||
background-image: var(--ha-slider-background);
|
background-image: var(--ha-slider-background);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
localeChanged
|
localeChanged
|
||||||
) {
|
) {
|
||||||
this._select.layoutOptions();
|
this._select.layoutOptions();
|
||||||
if (this._select.value !== this.value) {
|
if (!this.disabled && this._select.value !== this.value) {
|
||||||
fireEvent(this, "value-changed", { value: this._select.value });
|
fireEvent(this, "value-changed", { value: this._select.value });
|
||||||
}
|
}
|
||||||
if (!this.value) {
|
if (!this.value) {
|
||||||
@@ -141,7 +141,10 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const value =
|
const value =
|
||||||
this.value ?? (this.required ? languageOptions[0]?.value : this.value);
|
this.value ??
|
||||||
|
(this.required && !this.disabled
|
||||||
|
? languageOptions[0]?.value
|
||||||
|
: this.value);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
@@ -182,7 +185,7 @@ export class HaLanguagePicker extends LitElement {
|
|||||||
|
|
||||||
private _changed(ev): void {
|
private _changed(ev): void {
|
||||||
const target = ev.target as HaSelect;
|
const target = ev.target as HaSelect;
|
||||||
if (target.value === "" || target.value === this.value) {
|
if (this.disabled || target.value === "" || target.value === this.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.value = target.value;
|
this.value = target.value;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const haMdListStyles = [
|
|||||||
md-item {
|
md-item {
|
||||||
overflow: var(--md-item-overflow, hidden);
|
overflow: var(--md-item-overflow, hidden);
|
||||||
align-items: var(--md-item-align-items, center);
|
align-items: var(--md-item-align-items, center);
|
||||||
|
gap: var(--ha-md-list-item-gap, 16px);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { customElement } from "lit/decorators";
|
|||||||
|
|
||||||
@customElement("ha-outlined-icon-button")
|
@customElement("ha-outlined-icon-button")
|
||||||
export class HaOutlinedIconButton extends IconButton {
|
export class HaOutlinedIconButton extends IconButton {
|
||||||
|
protected override getRenderClasses() {
|
||||||
|
return {
|
||||||
|
...super.getRenderClasses(),
|
||||||
|
outlined: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
css`
|
css`
|
||||||
.icon-button {
|
.icon-button {
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ export class HaEntitySelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
|
||||||
<ha-entities-picker
|
<ha-entities-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.includeEntities=${this.selector.entity.include_entities}
|
.includeEntities=${this.selector.entity.include_entities}
|
||||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
|
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "sidebarPanelOrder",
|
key: "sidebarPanelOrder",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -217,6 +218,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
})
|
})
|
||||||
private _panelOrder: string[] = [];
|
private _panelOrder: string[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "sidebarHiddenPanels",
|
key: "sidebarHiddenPanels",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -850,8 +852,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
color: var(--sidebar-icon-color);
|
color: var(--sidebar-icon-color);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
margin-left: 19px;
|
margin-left: 3px;
|
||||||
margin-inline-start: 19px;
|
margin-inline-start: 3px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -938,7 +940,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
ha-md-list-item .item-text {
|
ha-md-list-item .item-text {
|
||||||
display: none;
|
display: none;
|
||||||
max-width: calc(100% - 56px);
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./device/ha-device-picker";
|
import "./device/ha-device-picker";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./entity/ha-entity-picker";
|
import "./entity/ha-entity-combo-box";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
import type { HaEntityComboBoxEntityFilterFunc } from "./entity/ha-entity-combo-box";
|
||||||
import "./ha-area-floor-picker";
|
import "./ha-area-floor-picker";
|
||||||
import { floorDefaultIconPath } from "./ha-floor-icon";
|
import { floorDefaultIconPath } from "./ha-floor-icon";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
@@ -80,7 +80,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@@ -449,7 +449,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
></ha-label-picker>
|
></ha-label-picker>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-entity-picker
|
<ha-entity-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
id="input"
|
id="input"
|
||||||
.type=${"entity_id"}
|
.type=${"entity_id"}
|
||||||
@@ -464,7 +464,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
@value-changed=${this._targetPicked}
|
@value-changed=${this._targetPicked}
|
||||||
@click=${this._preventDefault}
|
@click=${this._preventDefault}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
></ha-entity-picker>
|
></ha-entity-combo-box>
|
||||||
`}</mwc-menu-surface
|
`}</mwc-menu-surface
|
||||||
>`;
|
>`;
|
||||||
}
|
}
|
||||||
@@ -839,7 +839,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
mwc-menu-surface {
|
mwc-menu-surface {
|
||||||
--mdc-menu-min-width: 100%;
|
--mdc-menu-min-width: 100%;
|
||||||
}
|
}
|
||||||
ha-entity-picker,
|
ha-entity-combo-box,
|
||||||
ha-device-picker,
|
ha-device-picker,
|
||||||
ha-area-floor-picker {
|
ha-area-floor-picker {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export class HaToast extends Snackbar {
|
|||||||
max-width: 650px;
|
max-width: 650px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdc-snackbar__actions {
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
}
|
||||||
|
|
||||||
/* Revert the default styles set by mwc-snackbar */
|
/* Revert the default styles set by mwc-snackbar */
|
||||||
@media (max-width: 480px), (max-width: 344px) {
|
@media (max-width: 480px), (max-width: 344px) {
|
||||||
.mdc-snackbar__surface {
|
.mdc-snackbar__surface {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { mdiContentCopy } from "@mdi/js";
|
||||||
import { storage } from "../../common/decorators/storage";
|
import { storage } from "../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type {
|
import type {
|
||||||
@@ -17,6 +18,8 @@ import "../ha-language-picker";
|
|||||||
import "../ha-tts-voice-picker";
|
import "../ha-tts-voice-picker";
|
||||||
import "../ha-card";
|
import "../ha-card";
|
||||||
import { fetchCloudStatus } from "../../data/cloud";
|
import { fetchCloudStatus } from "../../data/cloud";
|
||||||
|
import { copyToClipboard } from "../../common/util/copy-clipboard";
|
||||||
|
import { showToast } from "../../util/toast";
|
||||||
|
|
||||||
export interface TtsMediaPickedEvent {
|
export interface TtsMediaPickedEvent {
|
||||||
item: MediaPlayerItem;
|
item: MediaPlayerItem;
|
||||||
@@ -42,6 +45,7 @@ class BrowseMediaTTS extends LitElement {
|
|||||||
|
|
||||||
@state() private _provider?: TTSEngine;
|
@state() private _provider?: TTSEngine;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "TtsMessage",
|
key: "TtsMessage",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -50,50 +54,69 @@ class BrowseMediaTTS extends LitElement {
|
|||||||
private _message?: string;
|
private _message?: string;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<ha-card>
|
return html`
|
||||||
<div class="card-content">
|
<ha-card>
|
||||||
<ha-textarea
|
<div class="card-content">
|
||||||
autogrow
|
<ha-textarea
|
||||||
.label=${this.hass.localize(
|
autogrow
|
||||||
"ui.components.media-browser.tts.message"
|
.label=${this.hass.localize(
|
||||||
)}
|
"ui.components.media-browser.tts.message"
|
||||||
.value=${this._message ||
|
)}
|
||||||
this.hass.localize(
|
.value=${this._message ||
|
||||||
"ui.components.media-browser.tts.example_message",
|
this.hass.localize(
|
||||||
{
|
"ui.components.media-browser.tts.example_message",
|
||||||
name: this.hass.user?.name || "Alice",
|
{
|
||||||
}
|
name: this.hass.user?.name || "Alice",
|
||||||
)}
|
}
|
||||||
>
|
)}
|
||||||
</ha-textarea>
|
>
|
||||||
${this._provider?.supported_languages?.length
|
</ha-textarea>
|
||||||
? html` <div class="options">
|
${this._provider?.supported_languages?.length
|
||||||
<ha-language-picker
|
? html` <div class="options">
|
||||||
.hass=${this.hass}
|
<ha-language-picker
|
||||||
.languages=${this._provider.supported_languages}
|
.hass=${this.hass}
|
||||||
.value=${this._language}
|
.languages=${this._provider.supported_languages}
|
||||||
required
|
.value=${this._language}
|
||||||
@value-changed=${this._languageChanged}
|
required
|
||||||
></ha-language-picker>
|
@value-changed=${this._languageChanged}
|
||||||
<ha-tts-voice-picker
|
></ha-language-picker>
|
||||||
.hass=${this.hass}
|
<ha-tts-voice-picker
|
||||||
.value=${this._voice}
|
.hass=${this.hass}
|
||||||
.engineId=${this._provider.engine_id}
|
.value=${this._voice}
|
||||||
.language=${this._language}
|
.engineId=${this._provider.engine_id}
|
||||||
required
|
.language=${this._language}
|
||||||
@value-changed=${this._voiceChanged}
|
required
|
||||||
></ha-tts-voice-picker>
|
@value-changed=${this._voiceChanged}
|
||||||
</div>`
|
></ha-tts-voice-picker>
|
||||||
: nothing}
|
</div>`
|
||||||
</div>
|
: nothing}
|
||||||
<div class="card-actions">
|
</div>
|
||||||
<mwc-button @click=${this._ttsClicked}>
|
<div class="card-actions">
|
||||||
${this.hass.localize(
|
<mwc-button @click=${this._ttsClicked}>
|
||||||
`ui.components.media-browser.tts.action_${this.action}`
|
${this.hass.localize(
|
||||||
)}
|
`ui.components.media-browser.tts.action_${this.action}`
|
||||||
</mwc-button>
|
)}
|
||||||
</div>
|
</mwc-button>
|
||||||
</ha-card> `;
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
${this._voice
|
||||||
|
? html`
|
||||||
|
<div class="footer">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.components.media-browser.tts.selected_voice_id`
|
||||||
|
)}
|
||||||
|
<code>${this._voice || "-"}</code>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiContentCopy}
|
||||||
|
@click=${this._copyVoiceId}
|
||||||
|
title=${this.hass.localize(
|
||||||
|
"ui.components.media-browser.tts.copy_voice_id"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override willUpdate(changedProps: PropertyValues): void {
|
protected override willUpdate(changedProps: PropertyValues): void {
|
||||||
@@ -196,6 +219,14 @@ class BrowseMediaTTS extends LitElement {
|
|||||||
fireEvent(this, "tts-picked", { item });
|
fireEvent(this, "tts-picked", { item });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _copyVoiceId(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
await copyToClipboard(this._voice);
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
buttonLinkStyle,
|
buttonLinkStyle,
|
||||||
css`
|
css`
|
||||||
@@ -217,6 +248,23 @@ class BrowseMediaTTS extends LitElement {
|
|||||||
button.link {
|
button.link {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: var(--ha-font-size-s);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.footer code {
|
||||||
|
font-weight: var(--ha-font-weight-bold);
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
--mdc-icon-size: 14px;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -890,12 +890,18 @@ export class HaMediaPlayerBrowse extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
margin-right: 48px;
|
margin-right: 48px;
|
||||||
|
margin-inline-end: 48px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight-add-button ha-svg-icon {
|
.highlight-add-button ha-svg-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
transform: scaleX(var(--scale-direction));
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class SearchInputOutlined extends LitElement {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
/* For iOS */
|
/* For iOS */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
}
|
}
|
||||||
ha-outlined-text-field {
|
ha-outlined-text-field {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -94,6 +93,8 @@ class SearchInputOutlined extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-svg-icon,
|
ha-svg-icon,
|
||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
height: var(--mdc-icon-button-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export class HaSlTabGroup extends TabGroup {
|
|||||||
|
|
||||||
--sl-color-neutral-600: inherit;
|
--sl-color-neutral-600: inherit;
|
||||||
|
|
||||||
--sl-font-weight-semibold: var(--ha-font-weight-semibold);
|
--sl-font-weight-semibold: var(--ha-font-weight-medium);
|
||||||
--sl-font-size-small: var(--ha-font-size-m);
|
--sl-font-size-small: var(--ha-font-size-m);
|
||||||
|
|
||||||
--sl-color-primary-600: var(
|
--sl-color-primary-600: var(
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export class HaTimeline extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public raised = false;
|
@property({ type: Boolean, reflect: true }) public raised = false;
|
||||||
|
|
||||||
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
|
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
|
||||||
false;
|
notEnabled = false;
|
||||||
|
|
||||||
@property({ attribute: "last-item", type: Boolean }) public lastItem = false;
|
@property({ attribute: "last-item", type: Boolean }) public lastItem = false;
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ export class HaTimeline extends LitElement {
|
|||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
:host([notEnabled]) ha-svg-icon {
|
:host([not-enabled]) ha-svg-icon {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
ha-svg-icon {
|
ha-svg-icon {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class HatGraphNode extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public error = false;
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|
||||||
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
|
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
|
||||||
false;
|
notEnabled = false;
|
||||||
|
|
||||||
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
||||||
graphStart = false;
|
graphStart = false;
|
||||||
@@ -127,13 +127,13 @@ export class HatGraphNode extends LitElement {
|
|||||||
--stroke-clr: var(--hover-clr);
|
--stroke-clr: var(--hover-clr);
|
||||||
--icon-clr: var(--default-icon-clr);
|
--icon-clr: var(--default-icon-clr);
|
||||||
}
|
}
|
||||||
:host([notEnabled]) circle {
|
:host([not-enabled]) circle {
|
||||||
--stroke-clr: var(--disabled-clr);
|
--stroke-clr: var(--disabled-clr);
|
||||||
}
|
}
|
||||||
:host([notEnabled][active]) circle {
|
:host([not-enabled][active]) circle {
|
||||||
--stroke-clr: var(--disabled-active-clr);
|
--stroke-clr: var(--disabled-active-clr);
|
||||||
}
|
}
|
||||||
:host([notEnabled]:hover) circle {
|
:host([not-enabled]:hover) circle {
|
||||||
--stroke-clr: var(--disabled-hover-clr);
|
--stroke-clr: var(--disabled-hover-clr);
|
||||||
}
|
}
|
||||||
svg:not(.safari) {
|
svg:not(.safari) {
|
||||||
|
|||||||
@@ -492,6 +492,25 @@ export const getAutomationEditorInitData = () => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isTrigger = (config: unknown): boolean => {
|
||||||
|
if (!config || typeof config !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const trigger = config as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
("trigger" in trigger && typeof trigger.trigger === "string") ||
|
||||||
|
("platform" in trigger && typeof trigger.platform === "string")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCondition = (config: unknown): boolean => {
|
||||||
|
if (!config || typeof config !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const condition = config as Record<string, unknown>;
|
||||||
|
return "condition" in condition && typeof condition.condition === "string";
|
||||||
|
};
|
||||||
|
|
||||||
export const subscribeTrigger = (
|
export const subscribeTrigger = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
onChange: (result: {
|
onChange: (result: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import {
|
import {
|
||||||
formatDurationLong,
|
formatDurationLong,
|
||||||
@@ -155,7 +155,7 @@ const tryDescribeTrigger = (
|
|||||||
|
|
||||||
const stateObj = Array.isArray(trigger.entity_id)
|
const stateObj = Array.isArray(trigger.entity_id)
|
||||||
? hass.states[trigger.entity_id[0]]
|
? hass.states[trigger.entity_id[0]]
|
||||||
: hass.states[trigger.entity_id];
|
: (hass.states[trigger.entity_id] as HassEntity | undefined);
|
||||||
|
|
||||||
if (Array.isArray(trigger.entity_id)) {
|
if (Array.isArray(trigger.entity_id)) {
|
||||||
for (const entity of trigger.entity_id.values()) {
|
for (const entity of trigger.entity_id.values()) {
|
||||||
@@ -172,12 +172,14 @@ const tryDescribeTrigger = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const attribute = trigger.attribute
|
const attribute = trigger.attribute
|
||||||
? computeAttributeNameDisplay(
|
? stateObj
|
||||||
hass.localize,
|
? computeAttributeNameDisplay(
|
||||||
stateObj,
|
hass.localize,
|
||||||
hass.entities,
|
stateObj,
|
||||||
trigger.attribute
|
hass.entities,
|
||||||
)
|
trigger.attribute
|
||||||
|
)
|
||||||
|
: trigger.attribute
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const duration = trigger.for
|
const duration = trigger.for
|
||||||
@@ -232,13 +234,15 @@ const tryDescribeTrigger = (
|
|||||||
if (trigger.attribute) {
|
if (trigger.attribute) {
|
||||||
const stateObj = Array.isArray(trigger.entity_id)
|
const stateObj = Array.isArray(trigger.entity_id)
|
||||||
? hass.states[trigger.entity_id[0]]
|
? hass.states[trigger.entity_id[0]]
|
||||||
: hass.states[trigger.entity_id];
|
: (hass.states[trigger.entity_id] as HassEntity | undefined);
|
||||||
attribute = computeAttributeNameDisplay(
|
attribute = stateObj
|
||||||
hass.localize,
|
? computeAttributeNameDisplay(
|
||||||
stateObj,
|
hass.localize,
|
||||||
hass.entities,
|
stateObj,
|
||||||
trigger.attribute
|
hass.entities,
|
||||||
);
|
trigger.attribute
|
||||||
|
)
|
||||||
|
: trigger.attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityArray: string[] = ensureArray(trigger.entity_id);
|
const entityArray: string[] = ensureArray(trigger.entity_id);
|
||||||
@@ -250,7 +254,7 @@ const tryDescribeTrigger = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateObj = hass.states[entityArray[0]];
|
const stateObj = hass.states[entityArray[0]] as HassEntity | undefined;
|
||||||
|
|
||||||
let fromChoice = "other";
|
let fromChoice = "other";
|
||||||
let fromString = "";
|
let fromString = "";
|
||||||
@@ -266,15 +270,17 @@ const tryDescribeTrigger = (
|
|||||||
const from: string[] = [];
|
const from: string[] = [];
|
||||||
for (const state of fromArray) {
|
for (const state of fromArray) {
|
||||||
from.push(
|
from.push(
|
||||||
trigger.attribute
|
stateObj
|
||||||
? hass
|
? trigger.attribute
|
||||||
.formatEntityAttributeValue(
|
? hass
|
||||||
stateObj,
|
.formatEntityAttributeValue(
|
||||||
trigger.attribute,
|
stateObj,
|
||||||
state
|
trigger.attribute,
|
||||||
)
|
state
|
||||||
.toString()
|
)
|
||||||
: hass.formatEntityState(stateObj, state)
|
.toString()
|
||||||
|
: hass.formatEntityState(stateObj, state)
|
||||||
|
: state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (from.length !== 0) {
|
if (from.length !== 0) {
|
||||||
@@ -298,15 +304,17 @@ const tryDescribeTrigger = (
|
|||||||
const to: string[] = [];
|
const to: string[] = [];
|
||||||
for (const state of toArray) {
|
for (const state of toArray) {
|
||||||
to.push(
|
to.push(
|
||||||
trigger.attribute
|
stateObj
|
||||||
? hass
|
? trigger.attribute
|
||||||
.formatEntityAttributeValue(
|
? hass
|
||||||
stateObj,
|
.formatEntityAttributeValue(
|
||||||
trigger.attribute,
|
stateObj,
|
||||||
state
|
trigger.attribute,
|
||||||
)
|
state
|
||||||
.toString()
|
)
|
||||||
: hass.formatEntityState(stateObj, state).toString()
|
.toString()
|
||||||
|
: hass.formatEntityState(stateObj, state).toString()
|
||||||
|
: state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (to.length !== 0) {
|
if (to.length !== 0) {
|
||||||
@@ -725,7 +733,9 @@ const tryDescribeTrigger = (
|
|||||||
if (localized) {
|
if (localized) {
|
||||||
return localized;
|
return localized;
|
||||||
}
|
}
|
||||||
const stateObj = hass.states[config.entity_id as string];
|
const stateObj = hass.states[config.entity_id as string] as
|
||||||
|
| HassEntity
|
||||||
|
| undefined;
|
||||||
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
|
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
|
||||||
config.type
|
config.type
|
||||||
}`;
|
}`;
|
||||||
@@ -894,13 +904,15 @@ const tryDescribeCondition = (
|
|||||||
if (condition.attribute) {
|
if (condition.attribute) {
|
||||||
const stateObj = Array.isArray(condition.entity_id)
|
const stateObj = Array.isArray(condition.entity_id)
|
||||||
? hass.states[condition.entity_id[0]]
|
? hass.states[condition.entity_id[0]]
|
||||||
: hass.states[condition.entity_id];
|
: (hass.states[condition.entity_id] as HassEntity | undefined);
|
||||||
attribute = computeAttributeNameDisplay(
|
attribute = stateObj
|
||||||
hass.localize,
|
? computeAttributeNameDisplay(
|
||||||
stateObj,
|
hass.localize,
|
||||||
hass.entities,
|
stateObj,
|
||||||
condition.attribute
|
hass.entities,
|
||||||
);
|
condition.attribute
|
||||||
|
)
|
||||||
|
: condition.attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entities: string[] = [];
|
const entities: string[] = [];
|
||||||
@@ -919,37 +931,40 @@ const tryDescribeCondition = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const states: string[] = [];
|
const states: string[] = [];
|
||||||
const stateObj =
|
const stateObj = hass.states[
|
||||||
hass.states[
|
Array.isArray(condition.entity_id)
|
||||||
Array.isArray(condition.entity_id)
|
? condition.entity_id[0]
|
||||||
? condition.entity_id[0]
|
: condition.entity_id
|
||||||
: condition.entity_id
|
] as HassEntity | undefined;
|
||||||
];
|
|
||||||
if (Array.isArray(condition.state)) {
|
if (Array.isArray(condition.state)) {
|
||||||
for (const state of condition.state.values()) {
|
for (const state of condition.state.values()) {
|
||||||
states.push(
|
states.push(
|
||||||
condition.attribute
|
stateObj
|
||||||
? hass
|
? condition.attribute
|
||||||
.formatEntityAttributeValue(
|
? hass
|
||||||
stateObj,
|
.formatEntityAttributeValue(
|
||||||
condition.attribute,
|
stateObj,
|
||||||
state
|
condition.attribute,
|
||||||
)
|
state
|
||||||
.toString()
|
)
|
||||||
: hass.formatEntityState(stateObj, state)
|
.toString()
|
||||||
|
: hass.formatEntityState(stateObj, state)
|
||||||
|
: state
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (condition.state !== "") {
|
} else if (condition.state !== "") {
|
||||||
states.push(
|
states.push(
|
||||||
condition.attribute
|
stateObj
|
||||||
? hass
|
? condition.attribute
|
||||||
.formatEntityAttributeValue(
|
? hass
|
||||||
stateObj,
|
.formatEntityAttributeValue(
|
||||||
condition.attribute,
|
stateObj,
|
||||||
condition.state
|
condition.attribute,
|
||||||
)
|
condition.state
|
||||||
.toString()
|
)
|
||||||
: hass.formatEntityState(stateObj, condition.state.toString())
|
.toString()
|
||||||
|
: hass.formatEntityState(stateObj, condition.state.toString())
|
||||||
|
: condition.state.toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -979,7 +994,7 @@ const tryDescribeCondition = (
|
|||||||
// Numeric State Condition
|
// Numeric State Condition
|
||||||
if (condition.condition === "numeric_state" && condition.entity_id) {
|
if (condition.condition === "numeric_state" && condition.entity_id) {
|
||||||
const entity_ids = ensureArray(condition.entity_id);
|
const entity_ids = ensureArray(condition.entity_id);
|
||||||
const stateObj = hass.states[entity_ids[0]];
|
const stateObj = hass.states[entity_ids[0]] as HassEntity | undefined;
|
||||||
const entity = formatListWithAnds(
|
const entity = formatListWithAnds(
|
||||||
hass.locale,
|
hass.locale,
|
||||||
entity_ids.map((id) =>
|
entity_ids.map((id) =>
|
||||||
@@ -988,12 +1003,14 @@ const tryDescribeCondition = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const attribute = condition.attribute
|
const attribute = condition.attribute
|
||||||
? computeAttributeNameDisplay(
|
? stateObj
|
||||||
hass.localize,
|
? computeAttributeNameDisplay(
|
||||||
stateObj,
|
hass.localize,
|
||||||
hass.entities,
|
stateObj,
|
||||||
condition.attribute
|
hass.entities,
|
||||||
)
|
condition.attribute
|
||||||
|
)
|
||||||
|
: condition.attribute
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (condition.above !== undefined && condition.below !== undefined) {
|
if (condition.above !== undefined && condition.below !== undefined) {
|
||||||
@@ -1187,7 +1204,9 @@ const tryDescribeCondition = (
|
|||||||
if (localized) {
|
if (localized) {
|
||||||
return localized;
|
return localized;
|
||||||
}
|
}
|
||||||
const stateObj = hass.states[config.entity_id as string];
|
const stateObj = hass.states[config.entity_id as string] as
|
||||||
|
| HassEntity
|
||||||
|
| undefined;
|
||||||
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
|
return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${
|
||||||
config.type
|
config.type
|
||||||
}`;
|
}`;
|
||||||
|
|||||||
@@ -167,10 +167,18 @@ const compareEntityIdWithEntityRegId = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (entityIdA.includes(".")) {
|
if (entityIdA.includes(".")) {
|
||||||
entityIdA = entityRegistryByEntityId(entityRegistry)[entityIdA].id;
|
const entityA = entityRegistryByEntityId(entityRegistry)[entityIdA];
|
||||||
|
if (!entityA) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entityIdA = entityA.id;
|
||||||
}
|
}
|
||||||
if (entityIdB.includes(".")) {
|
if (entityIdB.includes(".")) {
|
||||||
entityIdB = entityRegistryByEntityId(entityRegistry)[entityIdB].id;
|
const entityB = entityRegistryByEntityId(entityRegistry)[entityIdB];
|
||||||
|
if (!entityB) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entityIdB = entityB.id;
|
||||||
}
|
}
|
||||||
return entityIdA === entityIdB;
|
return entityIdA === entityIdB;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -795,10 +795,28 @@ export interface EnergySumData {
|
|||||||
from_battery?: number;
|
from_battery?: number;
|
||||||
solar?: number;
|
solar?: number;
|
||||||
};
|
};
|
||||||
|
timestamps: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnergyConsumptionData {
|
export interface EnergyConsumptionData {
|
||||||
total: Record<number, number>;
|
used_total: Record<number, number>;
|
||||||
|
grid_to_battery: Record<number, number>;
|
||||||
|
battery_to_grid: Record<number, number>;
|
||||||
|
solar_to_battery: Record<number, number>;
|
||||||
|
solar_to_grid: Record<number, number>;
|
||||||
|
used_solar: Record<number, number>;
|
||||||
|
used_grid: Record<number, number>;
|
||||||
|
used_battery: Record<number, number>;
|
||||||
|
total: {
|
||||||
|
used_total: number;
|
||||||
|
grid_to_battery: number;
|
||||||
|
battery_to_grid: number;
|
||||||
|
solar_to_battery: number;
|
||||||
|
solar_to_grid: number;
|
||||||
|
used_solar: number;
|
||||||
|
used_grid: number;
|
||||||
|
used_battery: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSummedData = memoizeOne(
|
export const getSummedData = memoizeOne(
|
||||||
@@ -867,7 +885,8 @@ const getSummedDataPartial = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const summedData: EnergySumData = { total: {} };
|
const summedData: EnergySumData = { total: {}, timestamps: [] };
|
||||||
|
const timestamps = new Set<number>();
|
||||||
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
||||||
const totalStats: Record<number, number> = {};
|
const totalStats: Record<number, number> = {};
|
||||||
const sets: Record<string, Record<number, number>> = {};
|
const sets: Record<string, Record<number, number>> = {};
|
||||||
@@ -886,6 +905,7 @@ const getSummedDataPartial = (
|
|||||||
sum += val;
|
sum += val;
|
||||||
totalStats[stat.start] =
|
totalStats[stat.start] =
|
||||||
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||||
|
timestamps.add(stat.start);
|
||||||
});
|
});
|
||||||
sets[id] = set;
|
sets[id] = set;
|
||||||
});
|
});
|
||||||
@@ -893,6 +913,8 @@ const getSummedDataPartial = (
|
|||||||
summedData.total[key] = sum;
|
summedData.total[key] = sum;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
summedData.timestamps = Array.from(timestamps).sort();
|
||||||
|
|
||||||
return summedData;
|
return summedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -915,25 +937,160 @@ export const computeConsumptionData = memoizeOne(
|
|||||||
const computeConsumptionDataPartial = (
|
const computeConsumptionDataPartial = (
|
||||||
data: EnergySumData
|
data: EnergySumData
|
||||||
): EnergyConsumptionData => {
|
): EnergyConsumptionData => {
|
||||||
const outData: EnergyConsumptionData = { total: {} };
|
const outData: EnergyConsumptionData = {
|
||||||
|
used_total: {},
|
||||||
|
grid_to_battery: {},
|
||||||
|
battery_to_grid: {},
|
||||||
|
solar_to_battery: {},
|
||||||
|
solar_to_grid: {},
|
||||||
|
used_solar: {},
|
||||||
|
used_grid: {},
|
||||||
|
used_battery: {},
|
||||||
|
total: {
|
||||||
|
used_total: 0,
|
||||||
|
grid_to_battery: 0,
|
||||||
|
battery_to_grid: 0,
|
||||||
|
solar_to_battery: 0,
|
||||||
|
solar_to_grid: 0,
|
||||||
|
used_solar: 0,
|
||||||
|
used_grid: 0,
|
||||||
|
used_battery: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
Object.keys(data).forEach((type) => {
|
data.timestamps.forEach((t) => {
|
||||||
Object.keys(data[type]).forEach((start) => {
|
const {
|
||||||
if (outData.total[start] === undefined) {
|
grid_to_battery,
|
||||||
const consumption =
|
battery_to_grid,
|
||||||
(data.from_grid?.[start] || 0) +
|
used_solar,
|
||||||
(data.solar?.[start] || 0) +
|
used_grid,
|
||||||
(data.from_battery?.[start] || 0) -
|
used_battery,
|
||||||
(data.to_grid?.[start] || 0) -
|
used_total,
|
||||||
(data.to_battery?.[start] || 0);
|
solar_to_battery,
|
||||||
outData.total[start] = consumption;
|
solar_to_grid,
|
||||||
}
|
} = computeConsumptionSingle({
|
||||||
|
from_grid: data.from_grid && (data.from_grid[t] ?? 0),
|
||||||
|
to_grid: data.to_grid && (data.to_grid[t] ?? 0),
|
||||||
|
solar: data.solar && (data.solar[t] ?? 0),
|
||||||
|
to_battery: data.to_battery && (data.to_battery[t] ?? 0),
|
||||||
|
from_battery: data.from_battery && (data.from_battery[t] ?? 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
outData.used_total[t] = used_total;
|
||||||
|
outData.total.used_total += used_total;
|
||||||
|
outData.grid_to_battery[t] = grid_to_battery;
|
||||||
|
outData.total.grid_to_battery += grid_to_battery;
|
||||||
|
outData.battery_to_grid![t] = battery_to_grid;
|
||||||
|
outData.total.battery_to_grid += battery_to_grid;
|
||||||
|
outData.used_battery![t] = used_battery;
|
||||||
|
outData.total.used_battery += used_battery;
|
||||||
|
outData.used_grid![t] = used_grid;
|
||||||
|
outData.total.used_grid += used_grid;
|
||||||
|
outData.used_solar![t] = used_solar;
|
||||||
|
outData.total.used_solar += used_solar;
|
||||||
|
outData.solar_to_battery[t] = solar_to_battery;
|
||||||
|
outData.total.solar_to_battery += solar_to_battery;
|
||||||
|
outData.solar_to_grid[t] = solar_to_grid;
|
||||||
|
outData.total.solar_to_grid += solar_to_grid;
|
||||||
});
|
});
|
||||||
|
|
||||||
return outData;
|
return outData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const computeConsumptionSingle = (data: {
|
||||||
|
from_grid: number | undefined;
|
||||||
|
to_grid: number | undefined;
|
||||||
|
solar: number | undefined;
|
||||||
|
to_battery: number | undefined;
|
||||||
|
from_battery: number | undefined;
|
||||||
|
}): {
|
||||||
|
grid_to_battery: number;
|
||||||
|
battery_to_grid: number;
|
||||||
|
solar_to_battery: number;
|
||||||
|
solar_to_grid: number;
|
||||||
|
used_solar: number;
|
||||||
|
used_grid: number;
|
||||||
|
used_battery: number;
|
||||||
|
used_total: number;
|
||||||
|
} => {
|
||||||
|
let to_grid = Math.max(data.to_grid || 0, 0);
|
||||||
|
let to_battery = Math.max(data.to_battery || 0, 0);
|
||||||
|
let solar = Math.max(data.solar || 0, 0);
|
||||||
|
let from_grid = Math.max(data.from_grid || 0, 0);
|
||||||
|
let from_battery = Math.max(data.from_battery || 0, 0);
|
||||||
|
|
||||||
|
const used_total =
|
||||||
|
(from_grid || 0) +
|
||||||
|
(solar || 0) +
|
||||||
|
(from_battery || 0) -
|
||||||
|
(to_grid || 0) -
|
||||||
|
(to_battery || 0);
|
||||||
|
|
||||||
|
let used_solar = 0;
|
||||||
|
let grid_to_battery = 0;
|
||||||
|
let battery_to_grid = 0;
|
||||||
|
let solar_to_battery = 0;
|
||||||
|
let solar_to_grid = 0;
|
||||||
|
let used_battery = 0;
|
||||||
|
let used_grid = 0;
|
||||||
|
|
||||||
|
let used_total_remaining = Math.max(used_total, 0);
|
||||||
|
// Consumption Priority
|
||||||
|
// Battery_Out -> Grid_Out
|
||||||
|
// Solar -> Grid_Out
|
||||||
|
// Solar -> Battery_In
|
||||||
|
// Grid_In -> Battery_In
|
||||||
|
// Solar -> Consumption
|
||||||
|
// Battery_Out -> Consumption
|
||||||
|
// Grid_In -> Consumption
|
||||||
|
|
||||||
|
// Battery_Out -> Grid_Out
|
||||||
|
battery_to_grid = Math.min(from_battery, to_grid);
|
||||||
|
from_battery -= battery_to_grid;
|
||||||
|
to_grid -= battery_to_grid;
|
||||||
|
|
||||||
|
// Solar -> Grid_Out
|
||||||
|
solar_to_grid = Math.min(solar, to_grid);
|
||||||
|
to_grid -= solar_to_grid;
|
||||||
|
solar -= solar_to_grid;
|
||||||
|
|
||||||
|
// Solar -> Battery_In
|
||||||
|
solar_to_battery = Math.min(solar, to_battery);
|
||||||
|
to_battery -= solar_to_battery;
|
||||||
|
solar -= solar_to_battery;
|
||||||
|
|
||||||
|
// Grid_In -> Battery_In
|
||||||
|
grid_to_battery = Math.min(from_grid, to_battery);
|
||||||
|
from_grid -= grid_to_battery;
|
||||||
|
to_battery -= grid_to_battery;
|
||||||
|
|
||||||
|
// Solar -> Consumption
|
||||||
|
used_solar = Math.min(used_total_remaining, solar);
|
||||||
|
used_total_remaining -= used_solar;
|
||||||
|
solar -= used_solar;
|
||||||
|
|
||||||
|
// Battery_Out -> Consumption
|
||||||
|
used_battery = Math.min(from_battery, used_total_remaining);
|
||||||
|
from_battery -= used_battery;
|
||||||
|
used_total_remaining -= used_battery;
|
||||||
|
|
||||||
|
// Grid_In -> Consumption
|
||||||
|
used_grid = Math.min(used_total_remaining, from_grid);
|
||||||
|
from_grid -= used_grid;
|
||||||
|
used_total_remaining -= from_grid;
|
||||||
|
|
||||||
|
return {
|
||||||
|
used_solar,
|
||||||
|
used_grid,
|
||||||
|
used_battery,
|
||||||
|
used_total,
|
||||||
|
grid_to_battery,
|
||||||
|
battery_to_grid,
|
||||||
|
solar_to_battery,
|
||||||
|
solar_to_grid,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const formatConsumptionShort = (
|
export const formatConsumptionShort = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
consumption: number | null,
|
consumption: number | null,
|
||||||
|
|||||||
@@ -107,3 +107,70 @@ export const DOMAIN_ATTRIBUTES_FORMATERS: Record<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NON_NUMERIC_ATTRIBUTES = [
|
||||||
|
"access_token",
|
||||||
|
"auto_update",
|
||||||
|
"available_modes",
|
||||||
|
"away_mode",
|
||||||
|
"changed_by",
|
||||||
|
"code_format",
|
||||||
|
"color_modes",
|
||||||
|
"current_activity",
|
||||||
|
"device_class",
|
||||||
|
"editable",
|
||||||
|
"effect_list",
|
||||||
|
"effect",
|
||||||
|
"entity_picture",
|
||||||
|
"event_type",
|
||||||
|
"event_types",
|
||||||
|
"fan_mode",
|
||||||
|
"fan_modes",
|
||||||
|
"fan_speed_list",
|
||||||
|
"forecast",
|
||||||
|
"friendly_name",
|
||||||
|
"frontend_stream_type",
|
||||||
|
"has_date",
|
||||||
|
"has_time",
|
||||||
|
"hs_color",
|
||||||
|
"hvac_mode",
|
||||||
|
"hvac_modes",
|
||||||
|
"icon",
|
||||||
|
"media_album_name",
|
||||||
|
"media_artist",
|
||||||
|
"media_content_type",
|
||||||
|
"media_position_updated_at",
|
||||||
|
"media_title",
|
||||||
|
"next_dawn",
|
||||||
|
"next_dusk",
|
||||||
|
"next_midnight",
|
||||||
|
"next_noon",
|
||||||
|
"next_rising",
|
||||||
|
"next_setting",
|
||||||
|
"operation_list",
|
||||||
|
"operation_mode",
|
||||||
|
"options",
|
||||||
|
"preset_mode",
|
||||||
|
"preset_modes",
|
||||||
|
"release_notes",
|
||||||
|
"release_summary",
|
||||||
|
"release_url",
|
||||||
|
"restored",
|
||||||
|
"rgb_color",
|
||||||
|
"rgbw_color",
|
||||||
|
"shuffle",
|
||||||
|
"sound_mode_list",
|
||||||
|
"sound_mode",
|
||||||
|
"source_list",
|
||||||
|
"source_type",
|
||||||
|
"source",
|
||||||
|
"state_class",
|
||||||
|
"supported_features",
|
||||||
|
"swing_mode",
|
||||||
|
"swing_mode",
|
||||||
|
"swing_modes",
|
||||||
|
"title",
|
||||||
|
"token",
|
||||||
|
"unit_of_measurement",
|
||||||
|
"xy_color",
|
||||||
|
];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const integrationsWithPanel = {
|
|||||||
dhcp: "config/dhcp",
|
dhcp: "config/dhcp",
|
||||||
matter: "config/matter",
|
matter: "config/matter",
|
||||||
mqtt: "config/mqtt",
|
mqtt: "config/mqtt",
|
||||||
|
ssdp: "config/ssdp",
|
||||||
thread: "config/thread",
|
thread: "config/thread",
|
||||||
zeroconf: "config/zeroconf",
|
zeroconf: "config/zeroconf",
|
||||||
zha: "config/zha/dashboard",
|
zha: "config/zha/dashboard",
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { computeDomain } from "../common/entity/compute_domain";
|
|||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "./entity";
|
import { UNAVAILABLE, UNKNOWN } from "./entity";
|
||||||
|
import type { HaEntityComboBoxEntityFilterFunc } from "../components/entity/ha-entity-combo-box";
|
||||||
|
|
||||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||||
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
|
export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"];
|
||||||
@@ -322,9 +322,8 @@ export const localizeStateMessage = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterLogbookCompatibleEntities: HaEntityPickerEntityFilterFunc = (
|
export const filterLogbookCompatibleEntities: HaEntityComboBoxEntityFilterFunc =
|
||||||
entity
|
(entity) =>
|
||||||
) =>
|
computeStateDomain(entity) !== "sensor" ||
|
||||||
computeStateDomain(entity) !== "sensor" ||
|
(entity.attributes.unit_of_measurement === undefined &&
|
||||||
(entity.attributes.unit_of_measurement === undefined &&
|
entity.attributes.state_class === undefined);
|
||||||
entity.attributes.state_class === undefined);
|
|
||||||
|
|||||||
@@ -474,3 +474,16 @@ export const migrateAutomationAction = (
|
|||||||
|
|
||||||
return action;
|
return action;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeScriptConfig = (config: ScriptConfig): ScriptConfig => {
|
||||||
|
// Normalize data: ensure sequence is a list
|
||||||
|
// Happens when people copy paste their scripts into the config
|
||||||
|
const value = config.sequence;
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
config.sequence = [value];
|
||||||
|
}
|
||||||
|
if (config.sequence) {
|
||||||
|
config.sequence = migrateAutomationAction(config.sequence);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|||||||
99
src/data/ssdp.ts
Normal file
99
src/data/ssdp.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
createCollection,
|
||||||
|
type Connection,
|
||||||
|
type UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
|
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import type { DataTableRowData } from "../components/data-table/ha-data-table";
|
||||||
|
|
||||||
|
export interface SSDPDiscoveryData extends DataTableRowData {
|
||||||
|
name: string | undefined;
|
||||||
|
ssdp_usn: string;
|
||||||
|
ssdp_st: string;
|
||||||
|
upnp: Record<string, unknown>;
|
||||||
|
ssdp_location: string | undefined;
|
||||||
|
ssdp_nt: string | undefined;
|
||||||
|
ssdp_udn: string | undefined;
|
||||||
|
ssdp_ext: string | undefined;
|
||||||
|
ssdp_server: string | undefined;
|
||||||
|
ssdp_headers: Record<string, unknown>;
|
||||||
|
ssdp_all_locations: string[];
|
||||||
|
x_homeassistant_matching_domains: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SSDPRemoveDiscoveryData {
|
||||||
|
ssdp_st: string;
|
||||||
|
ssdp_location: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SSDPSubscriptionMessage {
|
||||||
|
add?: SSDPDiscoveryData[];
|
||||||
|
change?: SSDPDiscoveryData[];
|
||||||
|
remove?: SSDPRemoveDiscoveryData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeSSDPDiscoveryUpdates = (
|
||||||
|
conn: Connection,
|
||||||
|
store: Store<SSDPDiscoveryData[]>
|
||||||
|
): Promise<UnsubscribeFunc> =>
|
||||||
|
conn.subscribeMessage<SSDPSubscriptionMessage>(
|
||||||
|
(event) => {
|
||||||
|
const data = [...(store.state || [])];
|
||||||
|
if (event.add) {
|
||||||
|
for (const deviceData of event.add) {
|
||||||
|
const index = data.findIndex(
|
||||||
|
(d) =>
|
||||||
|
d.ssdp_st === deviceData.ssdp_st &&
|
||||||
|
d.ssdp_location === deviceData.ssdp_location
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
data.push(deviceData);
|
||||||
|
} else {
|
||||||
|
data[index] = deviceData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.change) {
|
||||||
|
for (const deviceData of event.change) {
|
||||||
|
const index = data.findIndex(
|
||||||
|
(d) =>
|
||||||
|
d.ssdp_st === deviceData.ssdp_st &&
|
||||||
|
d.ssdp_location === deviceData.ssdp_location
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
data[index] = deviceData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.remove) {
|
||||||
|
for (const deviceData of event.remove) {
|
||||||
|
const index = data.findIndex(
|
||||||
|
(d) =>
|
||||||
|
d.ssdp_st === deviceData.ssdp_st &&
|
||||||
|
d.ssdp_location === deviceData.ssdp_location
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
data.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.setState(data, true);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: `ssdp/subscribe_discovery`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeSSDPDiscovery = (
|
||||||
|
conn: Connection,
|
||||||
|
callbackFunction: (ssdpDiscoveryData: SSDPDiscoveryData[]) => void
|
||||||
|
) =>
|
||||||
|
createCollection<SSDPDiscoveryData[]>(
|
||||||
|
"_ssdpDiscoveryRows",
|
||||||
|
() => Promise.resolve<SSDPDiscoveryData[]>([]), // empty array as initial state
|
||||||
|
|
||||||
|
subscribeSSDPDiscoveryUpdates,
|
||||||
|
conn,
|
||||||
|
callbackFunction
|
||||||
|
);
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
@@ -177,6 +176,17 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showDocumentationLink =
|
||||||
|
([
|
||||||
|
"form",
|
||||||
|
"menu",
|
||||||
|
"external",
|
||||||
|
"progress",
|
||||||
|
"data_entry_flow_progressed",
|
||||||
|
].includes(this._step?.type as any) &&
|
||||||
|
this._params.manifest?.is_built_in) ||
|
||||||
|
!!this._params.manifest?.documentation;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@@ -191,7 +201,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
<step-flow-loading
|
<step-flow-loading
|
||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.loadingReason=${this._loading}
|
.loadingReason=${this._loading!}
|
||||||
.handler=${this._handler}
|
.handler=${this._handler}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
></step-flow-loading>
|
></step-flow-loading>
|
||||||
@@ -199,26 +209,18 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
: this._step === undefined
|
: this._step === undefined
|
||||||
? // When we are going to next step, we render 1 round of empty
|
? // When we are going to next step, we render 1 round of empty
|
||||||
// to reset the element.
|
// to reset the element.
|
||||||
""
|
nothing
|
||||||
: html`
|
: html`
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
${([
|
${showDocumentationLink
|
||||||
"form",
|
|
||||||
"menu",
|
|
||||||
"external",
|
|
||||||
"progress",
|
|
||||||
"data_entry_flow_progressed",
|
|
||||||
].includes(this._step?.type as any) &&
|
|
||||||
this._params.manifest?.is_built_in) ||
|
|
||||||
this._params.manifest?.documentation
|
|
||||||
? html`
|
? html`
|
||||||
<a
|
<a
|
||||||
href=${this._params.manifest.is_built_in
|
href=${this._params.manifest!.is_built_in
|
||||||
? documentationUrl(
|
? documentationUrl(
|
||||||
this.hass,
|
this.hass,
|
||||||
`/integrations/${this._params.manifest.domain}`
|
`/integrations/${this._params.manifest!.domain}`
|
||||||
)
|
)
|
||||||
: this._params?.manifest?.documentation}
|
: this._params.manifest!.documentation}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
@@ -229,7 +231,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
</ha-icon-button
|
</ha-icon-button
|
||||||
></a>
|
></a>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
@@ -242,6 +244,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-form>
|
></step-flow-form>
|
||||||
`
|
`
|
||||||
: this._step.type === "external"
|
: this._step.type === "external"
|
||||||
@@ -250,6 +253,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-external>
|
></step-flow-external>
|
||||||
`
|
`
|
||||||
: this._step.type === "abort"
|
: this._step.type === "abort"
|
||||||
@@ -261,6 +265,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.handler=${this._step.handler}
|
.handler=${this._step.handler}
|
||||||
.domain=${this._params.domain ??
|
.domain=${this._params.domain ??
|
||||||
this._step.handler}
|
this._step.handler}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-abort>
|
></step-flow-abort>
|
||||||
`
|
`
|
||||||
: this._step.type === "progress"
|
: this._step.type === "progress"
|
||||||
@@ -270,6 +275,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.progress=${this._progress}
|
.progress=${this._progress}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-progress>
|
></step-flow-progress>
|
||||||
`
|
`
|
||||||
: this._step.type === "menu"
|
: this._step.type === "menu"
|
||||||
@@ -278,6 +284,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-menu>
|
></step-flow-menu>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -286,7 +293,8 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.navigateToResult=${this._params
|
.navigateToResult=${this._params
|
||||||
.navigateToResult}
|
.navigateToResult ?? false}
|
||||||
|
.increasePaddingEnd=${showDocumentationLink}
|
||||||
></step-flow-create-entry>
|
></step-flow-create-entry>
|
||||||
`}
|
`}
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class StepFlowAbort extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public handler!: string;
|
@property({ attribute: false }) public handler!: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
protected firstUpdated(changed: PropertyValues) {
|
protected firstUpdated(changed: PropertyValues) {
|
||||||
super.firstUpdated(changed);
|
super.firstUpdated(changed);
|
||||||
if (this.step.reason === "missing_credentials") {
|
if (this.step.reason === "missing_credentials") {
|
||||||
@@ -34,7 +37,7 @@ class StepFlowAbort extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
${this.params.flowConfig.renderAbortHeader
|
${this.params.flowConfig.renderAbortHeader
|
||||||
? this.params.flowConfig.renderAbortHeader(this.hass, this.step)
|
? this.params.flowConfig.renderAbortHeader(this.hass, this.step)
|
||||||
: this.hass.localize(`component.${this.domain}.title`)}
|
: this.hass.localize(`component.${this.domain}.title`)}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
public navigateToResult = false;
|
public navigateToResult = false;
|
||||||
|
|
||||||
@state() private _deviceUpdate: Record<
|
@state() private _deviceUpdate: Record<
|
||||||
@@ -113,7 +116,7 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
this.step.result?.entry_id
|
this.step.result?.entry_id
|
||||||
);
|
);
|
||||||
return html`
|
return html`
|
||||||
<h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
${devices.length
|
${devices.length
|
||||||
? localize("ui.panel.config.integrations.config_flow.assign_area", {
|
? localize("ui.panel.config.integrations.config_flow.assign_area", {
|
||||||
number: devices.length,
|
number: devices.length,
|
||||||
@@ -129,70 +132,73 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
)}</span
|
)}</span
|
||||||
>`
|
>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${devices.length === 0
|
${devices.length === 0 &&
|
||||||
? html`<p>
|
["options_flow", "repair_flow"].includes(this.flowConfig.flowType)
|
||||||
${localize(
|
? nothing
|
||||||
"ui.panel.config.integrations.config_flow.created_config",
|
: devices.length === 0
|
||||||
{ name: this.step.title }
|
? html`<p>
|
||||||
)}
|
${localize(
|
||||||
</p>`
|
"ui.panel.config.integrations.config_flow.created_config",
|
||||||
: html`
|
{ name: this.step.title }
|
||||||
<div class="devices">
|
|
||||||
${devices.map(
|
|
||||||
(device) => html`
|
|
||||||
<div class="device">
|
|
||||||
<div class="device-info">
|
|
||||||
${this.step.result?.domain
|
|
||||||
? html`<img
|
|
||||||
slot="graphic"
|
|
||||||
alt=${domainToName(
|
|
||||||
this.hass.localize,
|
|
||||||
this.step.result.domain
|
|
||||||
)}
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: this.step.result.domain,
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>`
|
|
||||||
: nothing}
|
|
||||||
<div class="device-info-details">
|
|
||||||
<span>${device.model || device.manufacturer}</span>
|
|
||||||
${device.model
|
|
||||||
? html`<span class="secondary">
|
|
||||||
${device.manufacturer}
|
|
||||||
</span>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ha-textfield
|
|
||||||
.label=${localize(
|
|
||||||
"ui.panel.config.integrations.config_flow.device_name"
|
|
||||||
)}
|
|
||||||
.placeholder=${computeDeviceNameDisplay(
|
|
||||||
device,
|
|
||||||
this.hass
|
|
||||||
)}
|
|
||||||
.value=${this._deviceUpdate[device.id]?.name ??
|
|
||||||
computeDeviceName(device)}
|
|
||||||
@change=${this._deviceNameChanged}
|
|
||||||
.device=${device.id}
|
|
||||||
></ha-textfield>
|
|
||||||
<ha-area-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.device=${device.id}
|
|
||||||
.value=${this._deviceUpdate[device.id]?.area ??
|
|
||||||
device.area_id ??
|
|
||||||
undefined}
|
|
||||||
@value-changed=${this._areaPicked}
|
|
||||||
></ha-area-picker>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</p>`
|
||||||
`}
|
: html`
|
||||||
|
<div class="devices">
|
||||||
|
${devices.map(
|
||||||
|
(device) => html`
|
||||||
|
<div class="device">
|
||||||
|
<div class="device-info">
|
||||||
|
${this.step.result?.domain
|
||||||
|
? html`<img
|
||||||
|
slot="graphic"
|
||||||
|
alt=${domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
this.step.result.domain
|
||||||
|
)}
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: this.step.result.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>`
|
||||||
|
: nothing}
|
||||||
|
<div class="device-info-details">
|
||||||
|
<span>${device.model || device.manufacturer}</span>
|
||||||
|
${device.model
|
||||||
|
? html`<span class="secondary">
|
||||||
|
${device.manufacturer}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ha-textfield
|
||||||
|
.label=${localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.device_name"
|
||||||
|
)}
|
||||||
|
.placeholder=${computeDeviceNameDisplay(
|
||||||
|
device,
|
||||||
|
this.hass
|
||||||
|
)}
|
||||||
|
.value=${this._deviceUpdate[device.id]?.name ??
|
||||||
|
computeDeviceName(device)}
|
||||||
|
@change=${this._deviceNameChanged}
|
||||||
|
.device=${device.id}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-area-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.device=${device.id}
|
||||||
|
.value=${this._deviceUpdate[device.id]?.area ??
|
||||||
|
device.area_id ??
|
||||||
|
undefined}
|
||||||
|
@value-changed=${this._areaPicked}
|
||||||
|
></ha-area-picker>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<mwc-button @click=${this._flowDone}
|
<mwc-button @click=${this._flowDone}
|
||||||
|
|||||||
@@ -15,11 +15,16 @@ class StepFlowExternal extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public step!: DataEntryFlowStepExternal;
|
@property({ attribute: false }) public step!: DataEntryFlowStepExternal;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const localize = this.hass.localize;
|
const localize = this.hass.localize;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<h2>${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}</h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
|
${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}
|
||||||
|
</h2>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
|
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
|
||||||
<div class="open-button">
|
<div class="open-button">
|
||||||
@@ -51,6 +56,9 @@ class StepFlowExternal extends LitElement {
|
|||||||
.open-button a {
|
.open-button a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
h2.end-space {
|
||||||
|
padding-inline-end: 72px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class StepFlowForm extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
@state() private _loading = false;
|
@state() private _loading = false;
|
||||||
|
|
||||||
@state() private _stepData?: Record<string, any>;
|
@state() private _stepData?: Record<string, any>;
|
||||||
@@ -43,7 +46,9 @@ class StepFlowForm extends LitElement {
|
|||||||
const stepData = this._stepDataProcessed;
|
const stepData = this._stepDataProcessed;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<h2>${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}</h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
|
${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}
|
||||||
|
</h2>
|
||||||
<div class="content" @click=${this._clickHandler}>
|
<div class="content" @click=${this._clickHandler}>
|
||||||
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
||||||
${this._errorMsg
|
${this._errorMsg
|
||||||
@@ -278,8 +283,6 @@ class StepFlowForm extends LitElement {
|
|||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding-inline-end: 72px;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ class StepFlowMenu extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
|
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
let options: string[];
|
let options: string[];
|
||||||
let translations: Record<string, string>;
|
let translations: Record<string, string>;
|
||||||
@@ -42,7 +45,9 @@ class StepFlowMenu extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
|
${this.flowConfig.renderMenuHeader(this.hass, this.step)}
|
||||||
|
</h2>
|
||||||
${description ? html`<div class="content">${description}</div>` : ""}
|
${description ? html`<div class="content">${description}</div>` : ""}
|
||||||
<div class="options">
|
<div class="options">
|
||||||
${options.map(
|
${options.map(
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ class StepFlowProgress extends LitElement {
|
|||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public progress?: number;
|
public progress?: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "increase-padding-end" })
|
||||||
|
public increasePaddingEnd = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<h2>
|
<h2 class=${this.increasePaddingEnd ? "end-space" : ""}>
|
||||||
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
|
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const configFlowContentStyles = css`
|
|||||||
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
|
text-transform: var(--mdc-typography-headline6-text-transform, inherit);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
h2.end-space {
|
||||||
|
padding-inline-end: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
.content,
|
.content,
|
||||||
.preview {
|
.preview {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class MoreInfoCover extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (positionStateDisplay) {
|
if (positionStateDisplay) {
|
||||||
return `${stateDisplay} ⸱ ${positionStateDisplay}`;
|
return `${stateDisplay} · ${positionStateDisplay}`;
|
||||||
}
|
}
|
||||||
return stateDisplay;
|
return stateDisplay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class MoreInfoValve extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (positionStateDisplay) {
|
if (positionStateDisplay) {
|
||||||
return `${stateDisplay} ⸱ ${positionStateDisplay}`;
|
return `${stateDisplay} · ${positionStateDisplay}`;
|
||||||
}
|
}
|
||||||
return stateDisplay;
|
return stateDisplay;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import {
|
|||||||
computeEntityEntryName,
|
computeEntityEntryName,
|
||||||
computeEntityName,
|
computeEntityName,
|
||||||
} from "../../common/entity/compute_entity_name";
|
} from "../../common/entity/compute_entity_name";
|
||||||
|
import {
|
||||||
|
getEntityContext,
|
||||||
|
getEntityEntryContext,
|
||||||
|
} from "../../common/entity/context/get_entity_context";
|
||||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import "../../components/ha-button-menu";
|
import "../../components/ha-button-menu";
|
||||||
@@ -58,10 +62,6 @@ import "./ha-more-info-history-and-logbook";
|
|||||||
import "./ha-more-info-info";
|
import "./ha-more-info-info";
|
||||||
import "./ha-more-info-settings";
|
import "./ha-more-info-settings";
|
||||||
import "./more-info-content";
|
import "./more-info-content";
|
||||||
import {
|
|
||||||
getEntityContext,
|
|
||||||
getEntityEntryContext,
|
|
||||||
} from "../../common/entity/get_entity_context";
|
|
||||||
|
|
||||||
export interface MoreInfoDialogParams {
|
export interface MoreInfoDialogParams {
|
||||||
entityId: string | null;
|
entityId: string | null;
|
||||||
|
|||||||
@@ -15,24 +15,26 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
import { canShowPage } from "../../common/config/can_show_page";
|
import { canShowPage } from "../../common/config/can_show_page";
|
||||||
import { componentsWithService } from "../../common/config/components_with_service";
|
import { componentsWithService } from "../../common/config/components_with_service";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
import {
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
computeDeviceName,
|
||||||
|
computeDeviceNameDisplay,
|
||||||
|
} from "../../common/entity/compute_device_name";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-label";
|
import "../../components/ha-label";
|
||||||
import "../../components/ha-list";
|
import "../../components/ha-list";
|
||||||
import "../../components/ha-list-item";
|
|
||||||
import "../../components/ha-spinner";
|
import "../../components/ha-spinner";
|
||||||
import "../../components/ha-textfield";
|
import "../../components/ha-textfield";
|
||||||
import "../../components/ha-tip";
|
import "../../components/ha-tip";
|
||||||
|
import "../../components/ha-md-list-item";
|
||||||
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||||
@@ -44,6 +46,13 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||||
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { HaFuse } from "../../resources/fuse";
|
||||||
|
|
||||||
interface QuickBarItem extends ScorableTextItem {
|
interface QuickBarItem extends ScorableTextItem {
|
||||||
primaryText: string;
|
primaryText: string;
|
||||||
@@ -59,6 +68,9 @@ interface CommandItem extends QuickBarItem {
|
|||||||
interface EntityItem extends QuickBarItem {
|
interface EntityItem extends QuickBarItem {
|
||||||
altText: string;
|
altText: string;
|
||||||
icon?: TemplateResult;
|
icon?: TemplateResult;
|
||||||
|
translatedDomain: string;
|
||||||
|
entityId: string;
|
||||||
|
friendlyName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceItem extends QuickBarItem {
|
interface DeviceItem extends QuickBarItem {
|
||||||
@@ -82,6 +94,7 @@ type BaseNavigationCommand = Pick<
|
|||||||
QuickBarNavigationItem,
|
QuickBarNavigationItem,
|
||||||
"primaryText" | "path"
|
"primaryText" | "path"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@customElement("ha-quick-bar")
|
@customElement("ha-quick-bar")
|
||||||
export class QuickBar extends LitElement {
|
export class QuickBar extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -139,6 +152,11 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this.hass.loadBackendTranslation("title");
|
||||||
|
}
|
||||||
|
|
||||||
private _getItems = memoizeOne(
|
private _getItems = memoizeOne(
|
||||||
(
|
(
|
||||||
mode: QuickBarMode,
|
mode: QuickBarMode,
|
||||||
@@ -323,61 +341,67 @@ export class QuickBar extends LitElement {
|
|||||||
|
|
||||||
private _renderDeviceItem(item: DeviceItem, index?: number) {
|
private _renderDeviceItem(item: DeviceItem, index?: number) {
|
||||||
return html`
|
return html`
|
||||||
<ha-list-item
|
<ha-md-list-item
|
||||||
.twoline=${Boolean(item.area)}
|
class="two-line"
|
||||||
.item=${item}
|
.item=${item}
|
||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span>${item.primaryText}</span>
|
<span slot="headline">${item.primaryText}</span>
|
||||||
${item.area
|
${item.area
|
||||||
? html`
|
? html` <span slot="supporting-text">${item.area}</span> `
|
||||||
<span slot="secondary" class="item-text secondary"
|
|
||||||
>${item.area}</span
|
|
||||||
>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderEntityItem(item: EntityItem, index?: number) {
|
private _renderEntityItem(item: EntityItem, index?: number) {
|
||||||
|
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-list-item
|
<ha-md-list-item
|
||||||
.twoline=${Boolean(item.altText)}
|
class=${showEntityId ? "three-line" : "two-line"}
|
||||||
.item=${item}
|
.item=${item}
|
||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
graphic="icon"
|
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
${item.iconPath
|
${item.iconPath
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${item.iconPath}
|
.path=${item.iconPath}
|
||||||
class="entity"
|
class="entity"
|
||||||
slot="graphic"
|
slot="start"
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
`
|
`
|
||||||
: html`<span slot="graphic">${item.icon}</span>`}
|
: html`<span slot="start">${item.icon}</span>`}
|
||||||
<span>${item.primaryText}</span>
|
<span slot="headline">${item.primaryText}</span>
|
||||||
${item.altText
|
${item.altText
|
||||||
|
? html` <span slot="supporting-text">${item.altText}</span> `
|
||||||
|
: nothing}
|
||||||
|
${item.entityId && showEntityId
|
||||||
? html`
|
? html`
|
||||||
<span slot="secondary" class="item-text secondary"
|
<span slot="supporting-text" class="code">${item.entityId}</span>
|
||||||
>${item.altText}</span
|
|
||||||
>
|
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-list-item>
|
${item.translatedDomain && !showEntityId
|
||||||
|
? html`<div slot="trailing-supporting-text">
|
||||||
|
${item.translatedDomain}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderCommandItem(item: CommandItem, index?: number) {
|
private _renderCommandItem(item: CommandItem, index?: number) {
|
||||||
return html`
|
return html`
|
||||||
<ha-list-item
|
<ha-md-list-item
|
||||||
.item=${item}
|
.item=${item}
|
||||||
index=${ifDefined(index)}
|
index=${ifDefined(index)}
|
||||||
hasMeta
|
hasMeta
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<ha-label
|
<ha-label
|
||||||
@@ -386,7 +410,10 @@ export class QuickBar extends LitElement {
|
|||||||
>
|
>
|
||||||
${item.iconPath
|
${item.iconPath
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon .path=${item.iconPath} slot="icon"></ha-svg-icon>
|
<ha-svg-icon
|
||||||
|
.path=${item.iconPath}
|
||||||
|
slot="start"
|
||||||
|
></ha-svg-icon>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${item.categoryText}
|
${item.categoryText}
|
||||||
@@ -394,7 +421,7 @@ export class QuickBar extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="command-text">${item.primaryText}</span>
|
<span class="command-text">${item.primaryText}</span>
|
||||||
</ha-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +448,7 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getItemAtIndex(index: number): ListItem | null {
|
private _getItemAtIndex(index: number): ListItem | null {
|
||||||
return this.renderRoot.querySelector(`ha-list-item[index="${index}"]`);
|
return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addSpinnerToCommandItem(index: number): void {
|
private _addSpinnerToCommandItem(index: number): void {
|
||||||
@@ -519,7 +546,7 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
const listItem = ev.target.closest("ha-list-item");
|
const listItem = ev.target.closest("ha-md-list-item");
|
||||||
this._processItemAndCloseDialog(
|
this._processItemAndCloseDialog(
|
||||||
listItem.item,
|
listItem.item,
|
||||||
Number(listItem.getAttribute("index"))
|
Number(listItem.getAttribute("index"))
|
||||||
@@ -555,18 +582,41 @@ export class QuickBar extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _generateEntityItems(): EntityItem[] {
|
private _generateEntityItems(): EntityItem[] {
|
||||||
|
const isRTL = computeRTL(this.hass);
|
||||||
|
|
||||||
return Object.keys(this.hass.states)
|
return Object.keys(this.hass.states)
|
||||||
.map((entityId) => {
|
.map((entityId) => {
|
||||||
const entityState = this.hass.states[entityId];
|
const stateObj = this.hass.states[entityId];
|
||||||
|
|
||||||
|
const { area, device } = getEntityContext(stateObj, this.hass);
|
||||||
|
|
||||||
|
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||||
|
const entityName = computeEntityName(stateObj, this.hass);
|
||||||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const primary = entityName || deviceName || entityId;
|
||||||
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
|
|
||||||
|
const translatedDomain = domainToName(
|
||||||
|
this.hass.localize,
|
||||||
|
computeDomain(entityId)
|
||||||
|
);
|
||||||
|
|
||||||
const entityItem = {
|
const entityItem = {
|
||||||
primaryText: computeStateName(entityState),
|
primaryText: primary,
|
||||||
altText: entityId,
|
altText: secondary,
|
||||||
icon: html`
|
icon: html`
|
||||||
<ha-state-icon
|
<ha-state-icon
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${entityState}
|
.stateObj=${stateObj}
|
||||||
></ha-state-icon>
|
></ha-state-icon>
|
||||||
`,
|
`,
|
||||||
|
translatedDomain: translatedDomain,
|
||||||
|
entityId: entityId,
|
||||||
|
friendlyName: friendlyName,
|
||||||
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
action: () => fireEvent(this, "hass-more-info", { entityId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -846,9 +896,30 @@ export class QuickBar extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _fuseIndex = memoizeOne((items: QuickBarItem[]) =>
|
||||||
|
Fuse.createIndex(
|
||||||
|
[
|
||||||
|
"primaryText",
|
||||||
|
"altText",
|
||||||
|
"friendlyName",
|
||||||
|
"translatedDomain",
|
||||||
|
"entityId", // for technical search
|
||||||
|
],
|
||||||
|
items
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
private _filterItems = memoizeOne(
|
private _filterItems = memoizeOne(
|
||||||
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
|
(items: QuickBarItem[], filter: string): QuickBarItem[] => {
|
||||||
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
|
const index = this._fuseIndex(items);
|
||||||
|
const fuse = new HaFuse(items, {}, index);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(filter.trim());
|
||||||
|
if (!results || !results.length) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
return results.map((result) => result.item);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
@@ -930,9 +1001,41 @@ export class QuickBar extends LitElement {
|
|||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-list-item {
|
ha-md-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
--mdc-list-item-graphic-margin: 20px;
|
}
|
||||||
|
|
||||||
|
/* Fixed height for items because we are use virtualizer */
|
||||||
|
ha-md-list-item.two-line {
|
||||||
|
--md-list-item-one-line-container-height: 64px;
|
||||||
|
--md-list-item-two-line-container-height: 64px;
|
||||||
|
--md-list-item-top-space: 8px;
|
||||||
|
--md-list-item-bottom-space: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item.three-line {
|
||||||
|
width: 100%;
|
||||||
|
--md-list-item-one-line-container-height: 72px;
|
||||||
|
--md-list-item-two-line-container-height: 72px;
|
||||||
|
--md-list-item-three-line-container-height: 72px;
|
||||||
|
--md-list-item-top-space: 8px;
|
||||||
|
--md-list-item-bottom-space: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item .code {
|
||||||
|
font-family: var(--ha-font-family-code);
|
||||||
|
font-size: var(--ha-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-md-list-item [slot="trailing-supporting-text"] {
|
||||||
|
font-size: var(--ha-font-size-s);
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
line-height: var(--ha-line-height-normal);
|
||||||
|
align-self: flex-end;
|
||||||
|
max-width: 30%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-tip {
|
ha-tip {
|
||||||
|
|||||||
@@ -68,6 +68,21 @@ const _SHORTCUTS: Section[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "ui.dialogs.shortcuts.automation_script.title",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "shortcut",
|
||||||
|
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"],
|
||||||
|
key: "ui.dialogs.shortcuts.automation_script.paste",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "shortcut",
|
||||||
|
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "S"],
|
||||||
|
key: "ui.dialogs.shortcuts.automation_script.save",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "ui.dialogs.shortcuts.charts.title",
|
key: "ui.dialogs.shortcuts.charts.title",
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
margin-inline-end: 12px;
|
margin-inline-end: 12px;
|
||||||
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "AssistPipelineId",
|
key: "AssistPipelineId",
|
||||||
state: true,
|
state: true,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import "../components/ha-spinner";
|
||||||
|
|
||||||
class HaInitPage extends LitElement {
|
class HaInitPage extends LitElement {
|
||||||
@property({ type: Boolean }) public error = false;
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class PanelCalendar extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string = undefined;
|
@state() private _error?: string = undefined;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "deSelectedCalendars",
|
key: "deSelectedCalendars",
|
||||||
state: true,
|
state: true,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
})
|
})
|
||||||
private _activeHiddenColumns?: string[];
|
private _activeHiddenColumns?: string[];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "application-credentials-table-search",
|
key: "application-credentials-table-search",
|
||||||
|
|||||||
@@ -674,6 +674,12 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
ha-tooltip {
|
ha-tooltip {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public actions!: Action[];
|
@property({ attribute: false }) public actions!: Action[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedActions?: Action[];
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "automationClipboard",
|
key: "automationClipboard",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -91,6 +94,7 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
@move-up=${this._moveUp}
|
@move-up=${this._moveUp}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
?highlight=${this.highlightedActions?.includes(action)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
|||||||
@@ -587,6 +587,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
ha-md-menu-item > ha-svg-icon {
|
ha-md-menu-item > ha-svg-icon {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public conditions!: Condition[];
|
@property({ attribute: false }) public conditions!: Condition[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedConditions?: Condition[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "automationClipboard",
|
key: "automationClipboard",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -140,6 +143,7 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
@move-up=${this._moveUp}
|
@move-up=${this._moveUp}
|
||||||
@value-changed=${this._conditionChanged}
|
@value-changed=${this._conditionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
?highlight=${this.highlightedConditions?.includes(cond)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import "../../../../../components/ha-form/ha-form";
|
|||||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||||
import type { NumericStateCondition } from "../../../../../data/automation";
|
import type { NumericStateCondition } from "../../../../../data/automation";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
|
import { NON_NUMERIC_ATTRIBUTES } from "../../../../../data/entity_attributes";
|
||||||
|
|
||||||
const numericStateConditionStruct = object({
|
const numericStateConditionStruct = object({
|
||||||
alias: optional(string()),
|
alias: optional(string()),
|
||||||
@@ -85,72 +86,7 @@ export default class HaNumericStateCondition extends LitElement {
|
|||||||
name: "attribute",
|
name: "attribute",
|
||||||
selector: {
|
selector: {
|
||||||
attribute: {
|
attribute: {
|
||||||
hide_attributes: [
|
hide_attributes: NON_NUMERIC_ATTRIBUTES,
|
||||||
"access_token",
|
|
||||||
"auto_update",
|
|
||||||
"available_modes",
|
|
||||||
"away_mode",
|
|
||||||
"changed_by",
|
|
||||||
"code_format",
|
|
||||||
"color_modes",
|
|
||||||
"current_activity",
|
|
||||||
"device_class",
|
|
||||||
"editable",
|
|
||||||
"effect_list",
|
|
||||||
"effect",
|
|
||||||
"entity_picture",
|
|
||||||
"event_type",
|
|
||||||
"event_types",
|
|
||||||
"fan_mode",
|
|
||||||
"fan_modes",
|
|
||||||
"fan_speed_list",
|
|
||||||
"forecast",
|
|
||||||
"friendly_name",
|
|
||||||
"frontend_stream_type",
|
|
||||||
"has_date",
|
|
||||||
"has_time",
|
|
||||||
"hs_color",
|
|
||||||
"hvac_mode",
|
|
||||||
"hvac_modes",
|
|
||||||
"icon",
|
|
||||||
"media_album_name",
|
|
||||||
"media_artist",
|
|
||||||
"media_content_type",
|
|
||||||
"media_position_updated_at",
|
|
||||||
"media_title",
|
|
||||||
"next_dawn",
|
|
||||||
"next_dusk",
|
|
||||||
"next_midnight",
|
|
||||||
"next_noon",
|
|
||||||
"next_rising",
|
|
||||||
"next_setting",
|
|
||||||
"operation_list",
|
|
||||||
"operation_mode",
|
|
||||||
"options",
|
|
||||||
"preset_mode",
|
|
||||||
"preset_modes",
|
|
||||||
"release_notes",
|
|
||||||
"release_summary",
|
|
||||||
"release_url",
|
|
||||||
"restored",
|
|
||||||
"rgb_color",
|
|
||||||
"rgbw_color",
|
|
||||||
"shuffle",
|
|
||||||
"sound_mode_list",
|
|
||||||
"sound_mode",
|
|
||||||
"source_list",
|
|
||||||
"source_type",
|
|
||||||
"source",
|
|
||||||
"state_class",
|
|
||||||
"supported_features",
|
|
||||||
"swing_mode",
|
|
||||||
"swing_mode",
|
|
||||||
"swing_modes",
|
|
||||||
"title",
|
|
||||||
"token",
|
|
||||||
"unit_of_measurement",
|
|
||||||
"xy_color",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { transform } from "../../../common/decorators/transform";
|
import { transform } from "../../../common/decorators/transform";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
@@ -81,6 +81,7 @@ import {
|
|||||||
} from "./automation-save-dialog/show-dialog-automation-save";
|
} from "./automation-save-dialog/show-dialog-automation-save";
|
||||||
import "./blueprint-automation-editor";
|
import "./blueprint-automation-editor";
|
||||||
import "./manual-automation-editor";
|
import "./manual-automation-editor";
|
||||||
|
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -134,6 +135,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
|
|
||||||
@state() private _blueprintConfig?: BlueprintAutomationConfig;
|
@state() private _blueprintConfig?: BlueprintAutomationConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||||
transformer: function (this: HaAutomationEditor, value) {
|
transformer: function (this: HaAutomationEditor, value) {
|
||||||
@@ -149,6 +151,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entityRegistry!: EntityRegistryEntry[];
|
_entityRegistry!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
@query("manual-automation-editor")
|
||||||
|
private _manualEditor?: HaManualAutomationEditor;
|
||||||
|
|
||||||
private _configSubscriptions: Record<
|
private _configSubscriptions: Record<
|
||||||
string,
|
string,
|
||||||
(config?: AutomationConfig) => void
|
(config?: AutomationConfig) => void
|
||||||
@@ -469,6 +474,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
.config=${this._config}
|
.config=${this._config}
|
||||||
.disabled=${Boolean(this._readOnly)}
|
.disabled=${Boolean(this._readOnly)}
|
||||||
|
.dirty=${this._dirty}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></manual-automation-editor>
|
></manual-automation-editor>
|
||||||
`}
|
`}
|
||||||
@@ -552,7 +558,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
} as AutomationConfig;
|
} as AutomationConfig;
|
||||||
this._entityId = undefined;
|
this._entityId = undefined;
|
||||||
this._readOnly = false;
|
this._readOnly = false;
|
||||||
this._dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("entityId") && this.entityId) {
|
if (changedProps.has("entityId") && this.entityId) {
|
||||||
@@ -952,6 +957,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._manualEditor?.resetPastedConfig();
|
||||||
|
|
||||||
const id = this.automationId || String(Date.now());
|
const id = this.automationId || String(Date.now());
|
||||||
if (!this.automationId) {
|
if (!this.automationId) {
|
||||||
const saved = await this._promptAutomationAlias();
|
const saved = await this._promptAutomationAlias();
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _filteredAutomations?: string[] | null;
|
@state() private _filteredAutomations?: string[] | null;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "automation-table-search",
|
key: "automation-table-search",
|
||||||
@@ -146,6 +147,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||||||
})
|
})
|
||||||
private _filter = "";
|
private _filter = "";
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "automation-table-filters-full",
|
key: "automation-table-filters-full",
|
||||||
|
|||||||
@@ -3,18 +3,35 @@ import { mdiHelpCircle } from "@mdi/js";
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { load } from "js-yaml";
|
||||||
|
import {
|
||||||
|
any,
|
||||||
|
array,
|
||||||
|
assert,
|
||||||
|
assign,
|
||||||
|
object,
|
||||||
|
optional,
|
||||||
|
string,
|
||||||
|
union,
|
||||||
|
} from "superstruct";
|
||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import type {
|
import type {
|
||||||
|
AutomationConfig,
|
||||||
Condition,
|
Condition,
|
||||||
ManualAutomationConfig,
|
ManualAutomationConfig,
|
||||||
Trigger,
|
Trigger,
|
||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
import type { Action } from "../../../data/script";
|
import {
|
||||||
|
isCondition,
|
||||||
|
isTrigger,
|
||||||
|
normalizeAutomationConfig,
|
||||||
|
} from "../../../data/automation";
|
||||||
|
import { getActionType, type Action } from "../../../data/script";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
@@ -29,6 +46,26 @@ import {
|
|||||||
removeSearchParam,
|
removeSearchParam,
|
||||||
} from "../../../common/url/search-params";
|
} from "../../../common/url/search-params";
|
||||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||||
|
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
|
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||||
|
|
||||||
|
const baseConfigStruct = object({
|
||||||
|
alias: optional(string()),
|
||||||
|
description: optional(string()),
|
||||||
|
triggers: optional(array(any())),
|
||||||
|
conditions: optional(array(any())),
|
||||||
|
actions: optional(array(any())),
|
||||||
|
mode: optional(string()),
|
||||||
|
max_exceeded: optional(string()),
|
||||||
|
id: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const automationConfigStruct = union([
|
||||||
|
assign(baseConfigStruct, object({ triggers: array(any()) })),
|
||||||
|
assign(baseConfigStruct, object({ conditions: array(any()) })),
|
||||||
|
assign(baseConfigStruct, object({ actions: array(any()) })),
|
||||||
|
]);
|
||||||
|
|
||||||
@customElement("manual-automation-editor")
|
@customElement("manual-automation-editor")
|
||||||
export class HaManualAutomationEditor extends LitElement {
|
export class HaManualAutomationEditor extends LitElement {
|
||||||
@@ -44,6 +81,22 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public dirty = false;
|
||||||
|
|
||||||
|
@state() private _pastedConfig?: ManualAutomationConfig;
|
||||||
|
|
||||||
|
private _previousConfig?: ManualAutomationConfig;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener("paste", this._handlePaste);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
window.removeEventListener("paste", this._handlePaste);
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
const expanded = extractSearchParam("expanded");
|
const expanded = extractSearchParam("expanded");
|
||||||
@@ -123,6 +176,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="triggers-heading"
|
aria-labelledby="triggers-heading"
|
||||||
.triggers=${this.config.triggers || []}
|
.triggers=${this.config.triggers || []}
|
||||||
|
.highlightedTriggers=${this._pastedConfig?.triggers || []}
|
||||||
.path=${["triggers"]}
|
.path=${["triggers"]}
|
||||||
@value-changed=${this._triggerChanged}
|
@value-changed=${this._triggerChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -164,6 +218,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="conditions-heading"
|
aria-labelledby="conditions-heading"
|
||||||
.conditions=${this.config.conditions || []}
|
.conditions=${this.config.conditions || []}
|
||||||
|
.highlightedConditions=${this._pastedConfig?.conditions || []}
|
||||||
.path=${["conditions"]}
|
.path=${["conditions"]}
|
||||||
@value-changed=${this._conditionChanged}
|
@value-changed=${this._conditionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -203,6 +258,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="actions-heading"
|
aria-labelledby="actions-heading"
|
||||||
.actions=${this.config.actions || []}
|
.actions=${this.config.actions || []}
|
||||||
|
.highlightedActions=${this._pastedConfig?.actions || []}
|
||||||
.path=${["actions"]}
|
.path=${["actions"]}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -214,6 +270,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _triggerChanged(ev: CustomEvent): void {
|
private _triggerChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, triggers: ev.detail.value as Trigger[] },
|
value: { ...this.config!, triggers: ev.detail.value as Trigger[] },
|
||||||
});
|
});
|
||||||
@@ -221,6 +278,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _conditionChanged(ev: CustomEvent): void {
|
private _conditionChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: {
|
value: {
|
||||||
...this.config!,
|
...this.config!,
|
||||||
@@ -231,6 +289,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _actionChanged(ev: CustomEvent): void {
|
private _actionChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, actions: ev.detail.value as Action[] },
|
value: { ...this.config!, actions: ev.detail.value as Action[] },
|
||||||
});
|
});
|
||||||
@@ -245,6 +304,216 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handlePaste = async (ev: ClipboardEvent) => {
|
||||||
|
if (!canOverrideAlphanumericInput(ev.composedPath())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paste = ev.clipboardData?.getData("text");
|
||||||
|
if (!paste) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loaded: any;
|
||||||
|
try {
|
||||||
|
loaded = load(paste);
|
||||||
|
} catch (_err: any) {
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.paste_invalid_yaml"
|
||||||
|
),
|
||||||
|
duration: 4000,
|
||||||
|
dismissable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded || typeof loaded !== "object") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = loaded;
|
||||||
|
|
||||||
|
if ("automation" in config) {
|
||||||
|
config = config.automation;
|
||||||
|
if (Array.isArray(config)) {
|
||||||
|
config = config[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(config)) {
|
||||||
|
if (config.length === 1) {
|
||||||
|
config = config[0];
|
||||||
|
} else {
|
||||||
|
const newConfig: AutomationConfig = {
|
||||||
|
triggers: [],
|
||||||
|
conditions: [],
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
let found = false;
|
||||||
|
config.forEach((cfg: any) => {
|
||||||
|
if (isTrigger(cfg)) {
|
||||||
|
found = true;
|
||||||
|
(newConfig.triggers as Trigger[]).push(cfg);
|
||||||
|
}
|
||||||
|
if (isCondition(cfg)) {
|
||||||
|
found = true;
|
||||||
|
(newConfig.conditions as Condition[]).push(cfg);
|
||||||
|
}
|
||||||
|
if (getActionType(cfg) !== "unknown") {
|
||||||
|
found = true;
|
||||||
|
(newConfig.actions as Action[]).push(cfg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found) {
|
||||||
|
config = newConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTrigger(config)) {
|
||||||
|
config = { triggers: [config] };
|
||||||
|
}
|
||||||
|
if (isCondition(config)) {
|
||||||
|
config = { conditions: [config] };
|
||||||
|
}
|
||||||
|
if (getActionType(config) !== "unknown") {
|
||||||
|
config = { actions: [config] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized: AutomationConfig;
|
||||||
|
|
||||||
|
try {
|
||||||
|
normalized = normalizeAutomationConfig(config);
|
||||||
|
} catch (_err: any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert(normalized, automationConfigStruct);
|
||||||
|
} catch (_err: any) {
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.paste_invalid_config"
|
||||||
|
),
|
||||||
|
duration: 4000,
|
||||||
|
dismissable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (this.dirty) {
|
||||||
|
const result = await new Promise<boolean>((resolve) => {
|
||||||
|
showPasteReplaceDialog(this, {
|
||||||
|
domain: "automation",
|
||||||
|
pastedConfig: normalized,
|
||||||
|
onClose: () => resolve(false),
|
||||||
|
onAppend: () => {
|
||||||
|
this._appendToExistingConfig(normalized);
|
||||||
|
resolve(false);
|
||||||
|
},
|
||||||
|
onReplace: () => resolve(true),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the config completely
|
||||||
|
this._replaceExistingConfig(normalized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private _appendToExistingConfig(config: ManualAutomationConfig) {
|
||||||
|
// make a copy otherwise we will reference the original config
|
||||||
|
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||||
|
this._pastedConfig = config;
|
||||||
|
|
||||||
|
if (!this.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("triggers" in config) {
|
||||||
|
this.config.triggers = ensureArray(this.config.triggers || []).concat(
|
||||||
|
ensureArray(config.triggers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("conditions" in config) {
|
||||||
|
this.config.conditions = ensureArray(this.config.conditions || []).concat(
|
||||||
|
ensureArray(config.conditions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("actions" in config) {
|
||||||
|
this.config.actions = ensureArray(this.config.actions || []).concat(
|
||||||
|
ensureArray(config.actions)
|
||||||
|
) as Action[];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showPastedToastWithUndo();
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.config!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _replaceExistingConfig(config: ManualAutomationConfig) {
|
||||||
|
// make a copy otherwise we will reference the original config
|
||||||
|
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||||
|
this._pastedConfig = config;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
this._showPastedToastWithUndo();
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showPastedToastWithUndo() {
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.paste_toast_message"
|
||||||
|
),
|
||||||
|
duration: 4000,
|
||||||
|
action: {
|
||||||
|
text: this.hass.localize("ui.common.undo"),
|
||||||
|
action: () => {
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this._previousConfig!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._previousConfig = undefined;
|
||||||
|
this._pastedConfig = undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetPastedConfig() {
|
||||||
|
if (!this._previousConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pastedConfig = undefined;
|
||||||
|
this._previousConfig = undefined;
|
||||||
|
|
||||||
|
showToast(this, {
|
||||||
|
message: "",
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default class HaAutomationOption extends LitElement {
|
|||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "automationClipboard",
|
key: "automationClipboard",
|
||||||
state: true,
|
state: true,
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||||
|
import "../trigger/ha-automation-trigger-row";
|
||||||
|
import type { PasteReplaceDialogParams } from "./show-dialog-paste-replace";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-paste-replace")
|
||||||
|
class DialogPasteReplace extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _params!: PasteReplaceDialogParams;
|
||||||
|
|
||||||
|
public showDialog(params: PasteReplaceDialogParams): void {
|
||||||
|
this._opened = true;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
if (this._opened) {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
this._opened = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this._opened) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.config.${this._params.domain}.editor.paste_confirm.title`
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.${this._params.domain}.editor.paste_confirm.text`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.defaultValue=${this._params?.pastedConfig}
|
||||||
|
read-only
|
||||||
|
></ha-yaml-editor>
|
||||||
|
|
||||||
|
<div slot="primaryAction">
|
||||||
|
<ha-button @click=${this._handleAppend}>
|
||||||
|
${this.hass.localize("ui.common.append")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._handleReplace}>
|
||||||
|
${this.hass.localize("ui.common.replace")}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleReplace() {
|
||||||
|
this._params?.onReplace();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAppend() {
|
||||||
|
this._params?.onAppend();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-paste-replace": DialogPasteReplace;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import type { AutomationConfig } from "../../../../data/automation";
|
||||||
|
import type { ScriptConfig } from "../../../../data/script";
|
||||||
|
|
||||||
|
export const loadPasteReplaceDialog = () => import("./dialog-paste-replace");
|
||||||
|
|
||||||
|
interface BasePasteReplaceDialogParams<D, T> {
|
||||||
|
domain: D;
|
||||||
|
pastedConfig: T;
|
||||||
|
onClose: () => void;
|
||||||
|
onAppend: () => void;
|
||||||
|
onReplace: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PasteReplaceDialogParams =
|
||||||
|
| BasePasteReplaceDialogParams<"automation", AutomationConfig>
|
||||||
|
| BasePasteReplaceDialogParams<"script", ScriptConfig>;
|
||||||
|
|
||||||
|
export const showPasteReplaceDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: PasteReplaceDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-paste-replace",
|
||||||
|
dialogImport: loadPasteReplaceDialog,
|
||||||
|
dialogParams: params,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -738,6 +738,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
ha-md-menu-item > ha-svg-icon {
|
ha-md-menu-item > ha-svg-icon {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,13 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public triggers!: Trigger[];
|
@property({ attribute: false }) public triggers!: Trigger[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedTriggers?: Trigger[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "automationClipboard",
|
key: "automationClipboard",
|
||||||
state: true,
|
state: true,
|
||||||
@@ -92,6 +95,7 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
@value-changed=${this._triggerChanged}
|
@value-changed=${this._triggerChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
?highlight=${this.highlightedTriggers?.includes(trg)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js";
|
import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||||
|
import { join } from "lit/directives/join";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
@@ -57,26 +58,51 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const texts: (TemplateResult | string)[] = [];
|
||||||
|
|
||||||
|
if (isNetworkMountAgent(agentId)) {
|
||||||
|
texts.push(
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.backup.agents.network_mount_agent_description"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const encryptionTurnedOff =
|
const encryptionTurnedOff =
|
||||||
this.agentsConfig?.[agentId]?.protected === false;
|
this.agentsConfig?.[agentId]?.protected === false;
|
||||||
|
|
||||||
if (encryptionTurnedOff) {
|
if (encryptionTurnedOff) {
|
||||||
return html`
|
texts.push(
|
||||||
<span class="dot warning"></span>
|
html`<div class="unencrypted-warning">
|
||||||
<span>
|
<span class="dot warning"></span>
|
||||||
${this.hass.localize(
|
<span>
|
||||||
"ui.panel.config.backup.agents.encryption_turned_off"
|
${this.hass.localize(
|
||||||
)}
|
"ui.panel.config.backup.agents.encryption_turned_off"
|
||||||
</span>
|
)}
|
||||||
`;
|
</span>
|
||||||
}
|
</div>`
|
||||||
|
|
||||||
if (isNetworkMountAgent(agentId)) {
|
|
||||||
return this.hass.localize(
|
|
||||||
"ui.panel.config.backup.agents.network_mount_agent_description"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
|
const retention = this.agentsConfig?.[agentId]?.retention;
|
||||||
|
|
||||||
|
if (retention) {
|
||||||
|
if (retention.copies === null && retention.days === null) {
|
||||||
|
texts.push(
|
||||||
|
this.hass.localize("ui.panel.config.backup.agents.retention_all")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
texts.push(
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.config.backup.agents.retention_${retention.copies ? "backups" : "days"}`,
|
||||||
|
{
|
||||||
|
count: retention.copies || retention.days,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return join(texts, html`<span class="separator"> · </span>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _availableAgents = memoizeOne(
|
private _availableAgents = memoizeOne(
|
||||||
@@ -287,6 +313,11 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
|
.unencrypted-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
.dot {
|
.dot {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -294,11 +325,22 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
height: 8px;
|
height: 8px;
|
||||||
background-color: var(--disabled-color);
|
background-color: var(--disabled-color);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex: none;
|
|
||||||
}
|
}
|
||||||
.dot.warning {
|
.dot.warning {
|
||||||
background-color: var(--warning-color);
|
background-color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
@media all and (max-width: 500px) {
|
||||||
|
.separator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
ha-md-list-item [slot="supporting-text"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _selected: string[] = [];
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "backups-table-filters",
|
key: "backups-table-filters",
|
||||||
|
|||||||
@@ -118,19 +118,17 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: this.config?.agents[this.agentId]
|
: html`<ha-backup-config-retention
|
||||||
? html`<ha-backup-config-retention
|
location-specific
|
||||||
location-specific
|
.headline=${this.hass.localize(
|
||||||
.headline=${this.hass.localize(
|
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
|
||||||
`ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`,
|
{ location: agentName }
|
||||||
{ location: agentName }
|
)}
|
||||||
)}
|
.hass=${this.hass}
|
||||||
.hass=${this.hass}
|
.retention=${this.config?.agents[this.agentId]
|
||||||
.retention=${this.config?.agents[this.agentId]
|
?.retention}
|
||||||
?.retention}
|
@value-changed=${this._retentionChanged}
|
||||||
@value-changed=${this._retentionChanged}
|
></ha-backup-config-retention>`}
|
||||||
></ha-backup-config-retention>`
|
|
||||||
: nothing}
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, html } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
@@ -118,6 +118,7 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
})
|
})
|
||||||
private _activeHiddenColumns?: string[];
|
private _activeHiddenColumns?: string[];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "blueprint-table-search",
|
key: "blueprint-table-search",
|
||||||
@@ -499,9 +500,11 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
list: html`<ul>
|
list: html`<ul>
|
||||||
${[...(related.automation || []), ...(related.script || [])].map(
|
${[...(related.automation || []), ...(related.script || [])].map(
|
||||||
(item) => {
|
(item) => {
|
||||||
const state = this.hass.states[item];
|
const automationState = this.hass.states[item];
|
||||||
return html`<li>
|
return html`<li>
|
||||||
${state ? `${computeStateName(state)} (${item})` : item}
|
${automationState
|
||||||
|
? `${computeStateName(automationState)} (${item})`
|
||||||
|
: item}
|
||||||
</li>`;
|
</li>`;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||||
|
import { getDeviceContext } from "../../../common/entity/context/get_device_context";
|
||||||
import "../../../components/entity/state-badge";
|
import "../../../components/entity/state-badge";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
@@ -76,6 +77,12 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
? this.getDeviceEntry(entityEntry.device_id)
|
? this.getDeviceEntry(entityEntry.device_id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const areaName =
|
||||||
|
deviceEntry && deviceEntry.entry_type !== "service"
|
||||||
|
? getDeviceContext(deviceEntry, this.hass).area?.name ||
|
||||||
|
this.hass.localize("ui.panel.config.updates.no_area")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
class=${ifDefined(
|
class=${ifDefined(
|
||||||
@@ -106,7 +113,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
"ui.panel.config.updates.update_in_progress"
|
"ui.panel.config.updates.update_in_progress"
|
||||||
)}
|
)}
|
||||||
></ha-spinner>`
|
></ha-spinner>`
|
||||||
: ""}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
>${deviceEntry
|
>${deviceEntry
|
||||||
@@ -114,10 +121,11 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
: entity.attributes.friendly_name}</span
|
: entity.attributes.friendly_name}</span
|
||||||
>
|
>
|
||||||
<span slot="supporting-text">
|
<span slot="supporting-text">
|
||||||
|
${areaName ? html`${areaName} ⸱ ` : nothing}
|
||||||
${entity.attributes.title} ${entity.attributes.latest_version}
|
${entity.attributes.title} ${entity.attributes.latest_version}
|
||||||
${entity.attributes.skipped_version
|
${entity.attributes.skipped_version
|
||||||
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
|
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
|
||||||
: ""}
|
: nothing}
|
||||||
</span>
|
</span>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? entity.attributes.in_progress
|
? entity.attributes.in_progress
|
||||||
@@ -130,7 +138,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
|||||||
></ha-spinner>
|
></ha-spinner>
|
||||||
</div>`
|
</div>`
|
||||||
: html`<ha-icon-next slot="end"></ha-icon-next>`
|
: html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||||
: ""}
|
: nothing}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class HaDeviceCard extends LitElement {
|
|||||||
<div class="extra-info">
|
<div class="extra-info">
|
||||||
${type === "bluetooth" &&
|
${type === "bluetooth" &&
|
||||||
isComponentLoaded(this.hass, "bluetooth")
|
isComponentLoaded(this.hass, "bluetooth")
|
||||||
? html`${titleCase(type)}
|
? html`${titleCase(type)}:
|
||||||
<a
|
<a
|
||||||
href="/config/bluetooth/advertisement-monitor?${createSearchParam(
|
href="/config/bluetooth/advertisement-monitor?${createSearchParam(
|
||||||
{ address: value }
|
{ address: value }
|
||||||
@@ -114,7 +114,7 @@ export class HaDeviceCard extends LitElement {
|
|||||||
>${value.toUpperCase()}</a
|
>${value.toUpperCase()}</a
|
||||||
>`
|
>`
|
||||||
: type === "mac" && isComponentLoaded(this.hass, "dhcp")
|
: type === "mac" && isComponentLoaded(this.hass, "dhcp")
|
||||||
? html`${titleCase(type)}
|
? html`MAC:
|
||||||
<a
|
<a
|
||||||
href="/config/dhcp?${createSearchParam({
|
href="/config/dhcp?${createSearchParam({
|
||||||
mac_address: value,
|
mac_address: value,
|
||||||
|
|||||||
@@ -1559,6 +1559,7 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-inline-start: 8px;
|
padding-inline-start: 8px;
|
||||||
|
padding-inline-end: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _selected: string[] = [];
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "devices-table-search",
|
key: "devices-table-search",
|
||||||
@@ -128,6 +129,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
})
|
})
|
||||||
private _filter: string = history.state?.filter || "";
|
private _filter: string = history.state?.filter || "";
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "devices-table-filters-full",
|
key: "devices-table-filters-full",
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entities!: EntityRegistryEntry[];
|
_entities!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "entities-table-search",
|
key: "entities-table-search",
|
||||||
@@ -169,6 +170,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "entities-table-filters",
|
key: "entities-table-filters",
|
||||||
|
|||||||
@@ -560,6 +560,11 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
|||||||
load: () =>
|
load: () =>
|
||||||
import("./integrations/integration-panels/dhcp/dhcp-config-panel"),
|
import("./integrations/integration-panels/dhcp/dhcp-config-panel"),
|
||||||
},
|
},
|
||||||
|
ssdp: {
|
||||||
|
tag: "ssdp-config-panel",
|
||||||
|
load: () =>
|
||||||
|
import("./integrations/integration-panels/ssdp/ssdp-config-panel"),
|
||||||
|
},
|
||||||
zeroconf: {
|
zeroconf: {
|
||||||
tag: "zeroconf-config-panel",
|
tag: "zeroconf-config-panel",
|
||||||
load: () =>
|
load: () =>
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
})
|
})
|
||||||
private _activeCollapsed?: string;
|
private _activeCollapsed?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
storage: "sessionStorage",
|
storage: "sessionStorage",
|
||||||
key: "helpers-table-search",
|
key: "helpers-table-search",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user