mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-25 00:59:29 +00:00
Compare commits
1 Commits
move-defau
...
fix-media-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bf7fe5caf8 |
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
- name: Send bundle stats and build information to RelativeCI
|
||||||
uses: relative-ci/agent-action@v3.0.0
|
uses: relative-ci/agent-action@v2.2.0
|
||||||
with:
|
with:
|
||||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
@@ -1 +1 @@
|
|||||||
yarn run lint-staged --relative
|
yarn run lint-staged --relative --shell "/bin/bash"
|
||||||
|
18
.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch
Normal file
18
.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
diff --git a/dist/hls.light.mjs b/dist/hls.light.mjs
|
||||||
|
index eed9d788fafdb159975e1a2eb08ac88ba9c9ac33..ace881935e6665946f1c8110ebd2f739cde4427e 100644
|
||||||
|
--- a/dist/hls.light.mjs
|
||||||
|
+++ b/dist/hls.light.mjs
|
||||||
|
@@ -20523,9 +20523,9 @@ class Hls {
|
||||||
|
}
|
||||||
|
Hls.defaultConfig = void 0;
|
||||||
|
|
||||||
|
-var KeySystemFormats = empty.KeySystemFormats;
|
||||||
|
-var KeySystems = empty.KeySystems;
|
||||||
|
-var SubtitleStreamController = empty.SubtitleStreamController;
|
||||||
|
-var TimelineController = empty.TimelineController;
|
||||||
|
+var KeySystemFormats = empty;
|
||||||
|
+var KeySystems = empty;
|
||||||
|
+var SubtitleStreamController = empty;
|
||||||
|
+var TimelineController = empty;
|
||||||
|
export { AbrController, AttrList, Cues as AudioStreamController, Cues as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, Cues as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, Cues as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, Cues as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
|
||||||
|
//# sourceMappingURL=hls.light.mjs.map
|
@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
|
|||||||
}
|
}
|
||||||
.error {
|
.error {
|
||||||
color: red;
|
color: red;
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error a {
|
.error a {
|
||||||
|
@@ -86,9 +86,9 @@ class HcLayout extends LitElement {
|
|||||||
.card-header {
|
.card-header {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 32px;
|
||||||
padding: 24px 16px 16px;
|
padding: 24px 16px 16px;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -98,7 +98,7 @@ class HcLayout extends LitElement {
|
|||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class HcLayout extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:host ::slotted(.section-header) {
|
:host ::slotted(.section-header) {
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,7 @@ class HcLayout extends LitElement {
|
|||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 12px;
|
||||||
padding: 8px 0 24px;
|
padding: 8px 0 24px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@ class HcLaunchScreen extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f2f4f9;
|
background-color: #f2f4f9;
|
||||||
font-size: var(--ha-font-size-2xl);
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -68,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
|
margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
|
||||||
padding-top: 48px;
|
padding-top: 48px;
|
||||||
}
|
}
|
||||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
padding-top: 48px;
|
padding-top: 48px;
|
||||||
}
|
}
|
||||||
.ohf-logo {
|
.ohf-logo {
|
||||||
margin: max(var(--safe-area-inset-bottom), 48px) 0;
|
margin: max(env(safe-area-inset-bottom), 48px) 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -1,30 +1,7 @@
|
|||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
let changeFunction;
|
|
||||||
|
|
||||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||||
hass.mockWS("frontend/get_user_data", () => ({
|
hass.mockWS("frontend/get_user_data", () => ({
|
||||||
value: null,
|
value: null,
|
||||||
}));
|
}));
|
||||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
|
||||||
if (key === "sidebar") {
|
|
||||||
changeFunction?.({
|
|
||||||
value: {
|
|
||||||
panelOrder: value.panelOrder || [],
|
|
||||||
hiddenPanels: value.hiddenPanels || [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
|
||||||
changeFunction = onChange;
|
|
||||||
onChange?.({
|
|
||||||
value: {
|
|
||||||
panelOrder: [],
|
|
||||||
hiddenPanels: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
return () => {};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@@ -38,12 +38,12 @@ class PageDescription extends HaMarkdown {
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 56px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 18px;
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
.root {
|
.root {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
@@ -34,7 +34,7 @@ class HaDemoOptions extends LitElement {
|
|||||||
height: 64px;
|
height: 64px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: var(--ha-font-size-xl);
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -250,14 +250,14 @@ class HaGallery extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-footer .header {
|
.page-footer .header {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 16px;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 28px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-footer .secondary {
|
.page-footer .secondary {
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 23px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -150,7 +150,7 @@ export class DemoHaBarButton extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--control-button-icon-color: var(--primary-color);
|
--control-button-icon-color: var(--primary-color);
|
||||||
|
@@ -86,7 +86,7 @@ export class DemoHarControlNumberButtons extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
color: #2196f3;
|
color: #2196f3;
|
||||||
|
@@ -125,7 +125,7 @@ export class DemoHaControlSelectMenu extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--control-button-icon-color: var(--primary-color);
|
--control-button-icon-color: var(--primary-color);
|
||||||
|
@@ -181,7 +181,7 @@ export class DemoHaControlSelect extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
|
@@ -144,7 +144,7 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--control-slider-color: #ffcf4c;
|
--control-slider-color: #ffcf4c;
|
||||||
|
@@ -112,7 +112,7 @@ export class DemoHaControlSwitch extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.custom {
|
.custom {
|
||||||
--control-switch-on-color: var(--green-color);
|
--control-switch-on-color: var(--green-color);
|
||||||
|
@@ -105,8 +105,8 @@ export class DemoHaHsColorPicker extends LitElement {
|
|||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
.value {
|
.value {
|
||||||
font-size: var(--ha-font-size-xl);
|
font-size: 22px;
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
margin: 0 0 12px 0;
|
margin: 0 0 12px 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -123,7 +123,7 @@ export class DemoHaSelectBox extends LitElement {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: 600;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { html, css, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
|
||||||
import "../../../../src/components/ha-bar";
|
import "../../../../src/components/ha-bar";
|
||||||
import "../../../../src/components/ha-card";
|
import "../../../../src/components/ha-card";
|
||||||
import "../../../../src/components/ha-spinner";
|
import "../../../../src/components/ha-spinner";
|
||||||
@@ -12,66 +11,29 @@ export class DemoHaSpinner extends LitElement {
|
|||||||
@property({ attribute: false }) hass!: HomeAssistant;
|
@property({ attribute: false }) hass!: HomeAssistant;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`<ha-card header="Basic spinner">
|
||||||
${["light", "dark"].map(
|
<div class="card-content">
|
||||||
(mode) => html`
|
<ha-spinner></ha-spinner></div
|
||||||
<div class=${mode}>
|
></ha-card>
|
||||||
<ha-card header="ha-badge ${mode} demo">
|
<ha-card header="Different spinner sizes">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<ha-spinner></ha-spinner>
|
<ha-spinner size="tiny"></ha-spinner>
|
||||||
<ha-spinner size="tiny"></ha-spinner>
|
<ha-spinner size="small"></ha-spinner>
|
||||||
<ha-spinner size="small"></ha-spinner>
|
<ha-spinner size="medium"></ha-spinner>
|
||||||
<ha-spinner size="medium"></ha-spinner>
|
<ha-spinner size="large"></ha-spinner></div
|
||||||
<ha-spinner size="large"></ha-spinner>
|
></ha-card>
|
||||||
<ha-spinner aria-label="Doing something..."></ha-spinner>
|
<ha-card header="Spinner with an aria-label">
|
||||||
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner>
|
<div class="card-content">
|
||||||
</div>
|
<ha-spinner aria-label="Doing something..."></ha-spinner>
|
||||||
</ha-card>
|
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner></div
|
||||||
</div>
|
></ha-card>`;
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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`
|
static styles = css`
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.dark,
|
|
||||||
.light {
|
|
||||||
display: block;
|
|
||||||
background-color: var(--primary-background-color);
|
|
||||||
padding: 0 50px;
|
|
||||||
margin: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
ha-card {
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
margin: 24px auto;
|
margin: 24px auto;
|
||||||
}
|
}
|
||||||
.card-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeDateTime extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -92,7 +92,7 @@ export class DemoDateTimeDate extends LitElement {
|
|||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -106,7 +106,7 @@ export class DemoDateTimeTime extends LitElement {
|
|||||||
margin: 12px auto;
|
margin: 12px auto;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -428,13 +428,13 @@ class HassioAddonConfig extends LitElement {
|
|||||||
.header h2 {
|
.header h2 {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
line-height: var(--ha-line-height-expanded);
|
line-height: 48px;
|
||||||
padding: 12px 16px 16px;
|
padding: 12px 16px 16px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-block: 0px;
|
margin-block: 0px;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
.card-actions.right {
|
.card-actions.right {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@@ -1280,12 +1280,12 @@ class HassioAddonInfo extends LitElement {
|
|||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-inline-start: 8px;
|
padding-inline-start: 8px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
font-size: var(--ha-font-size-2xl);
|
font-size: 24px;
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
}
|
}
|
||||||
.addon-version {
|
.addon-version {
|
||||||
float: var(--float-end);
|
float: var(--float-end);
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 15px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.errors {
|
.errors {
|
||||||
|
@@ -391,7 +391,7 @@ export class HassioBackups extends LitElement {
|
|||||||
top: -4px;
|
top: -4px;
|
||||||
}
|
}
|
||||||
.selected-txt {
|
.selected-txt {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-inline-start: 16px;
|
padding-inline-start: 16px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
@@ -401,7 +401,7 @@ export class HassioBackups extends LitElement {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.header-toolbar .selected-txt {
|
.header-toolbar .selected-txt {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.header-toolbar .header-btns {
|
.header-toolbar .header-btns {
|
||||||
margin-right: -12px;
|
margin-right: -12px;
|
||||||
|
@@ -101,7 +101,7 @@ class HassioCardContent extends LitElement {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 2.4em;
|
height: 2.4em;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 1.2em;
|
||||||
}
|
}
|
||||||
.icon_image img {
|
.icon_image img {
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
|
@@ -132,9 +132,9 @@ class HassioDashboard extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-fab.non-tabs {
|
ha-fab.non-tabs {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: calc(16px + var(--safe-area-inset-right));
|
right: calc(16px + env(safe-area-inset-right));
|
||||||
bottom: calc(16px + var(--safe-area-inset-bottom));
|
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||||
inset-inline-end: calc(16px + var(--safe-area-inset-right));
|
inset-inline-end: calc(16px + env(safe-area-inset-right));
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
@@ -131,7 +131,7 @@ export class HassioUpdate extends LitElement {
|
|||||||
}
|
}
|
||||||
.update-heading {
|
.update-heading {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: var(--ha-font-size-l);
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
@@ -173,7 +173,7 @@ class HassioHardwareDialog extends LitElement {
|
|||||||
font-family: var(--ha-font-family-code);
|
font-family: var(--ha-font-family-code);
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 85%;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
search-input {
|
search-input {
|
||||||
|
@@ -610,7 +610,7 @@ export class DialogHassioNetwork
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-bottom: max(var(--safe-area-inset-bottom), 8px);
|
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||||
background-color: var(--mdc-theme-surface, #fff);
|
background-color: var(--mdc-theme-surface, #fff);
|
||||||
}
|
}
|
||||||
.warning {
|
.warning {
|
||||||
|
@@ -1,8 +1,3 @@
|
|||||||
import {
|
|
||||||
haFontFamilyBody,
|
|
||||||
haFontSmoothing,
|
|
||||||
haMozOsxFontSmoothing,
|
|
||||||
} from "../../src/resources/theme/typography.globals";
|
|
||||||
import "./hassio-main";
|
import "./hassio-main";
|
||||||
|
|
||||||
import("../../src/resources/append-ha-style");
|
import("../../src/resources/append-ha-style");
|
||||||
@@ -10,10 +5,10 @@ import("../../src/resources/append-ha-style");
|
|||||||
const styleEl = document.createElement("style");
|
const styleEl = document.createElement("style");
|
||||||
styleEl.textContent = `
|
styleEl.textContent = `
|
||||||
body {
|
body {
|
||||||
font-family: ${haFontFamilyBody};
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: ${haFontSmoothing};
|
-webkit-font-smoothing: antialiased;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
@@ -340,12 +340,12 @@ class HassioIngressView extends LitElement {
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 16px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-color: var(--app-header-background-color);
|
background-color: var(--app-header-background-color);
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
color: var(--app-header-text-color, white);
|
color: var(--app-header-text-color, white);
|
||||||
border-bottom: var(--app-header-border-bottom, none);
|
border-bottom: var(--app-header-border-bottom, none);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -354,7 +354,7 @@ class HassioIngressView extends LitElement {
|
|||||||
|
|
||||||
.main-title {
|
.main-title {
|
||||||
margin: var(--margin-title);
|
margin: var(--margin-title);
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 20px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,7 +14,6 @@ export const hassioStyle = css`
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: var(--ha-font-family-body);
|
||||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
|
||||||
font-size: var(--ha-font-size-2xl);
|
font-size: var(--ha-font-size-2xl);
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: var(--ha-font-weight-normal);
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: var(--ha-line-height-condensed);
|
||||||
|
@@ -4,7 +4,7 @@ export default {
|
|||||||
"prettier --cache --write",
|
"prettier --cache --write",
|
||||||
"lit-analyzer --quiet",
|
"lit-analyzer --quiet",
|
||||||
],
|
],
|
||||||
"*.{json,css,md,markdown,html,ya?ml}": "prettier --cache --write",
|
"*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write",
|
||||||
"translations/*/*.json": (files) =>
|
"translations/*/*.json": (files) =>
|
||||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
||||||
files.join(" ") +
|
files.join(" ") +
|
||||||
|
67
package.json
67
package.json
@@ -26,15 +26,15 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.27.4",
|
"@babel/runtime": "7.27.0",
|
||||||
"@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",
|
||||||
"@codemirror/language": "6.11.0",
|
"@codemirror/language": "6.11.0",
|
||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.1",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.10",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.37.1",
|
"@codemirror/view": "6.36.6",
|
||||||
"@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,17 +89,17 @@
|
|||||||
"@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.7",
|
"@vaadin/combo-box": "24.7.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.7.7",
|
"@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",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"barcode-detector": "3.0.4",
|
"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.42.0",
|
"core-js": "3.41.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",
|
||||||
@@ -111,9 +111,9 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.4",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.7.16",
|
"intl-messageformat": "10.7.16",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
"lit": "3.3.0",
|
"lit": "3.3.0",
|
||||||
"lit-html": "3.3.0",
|
"lit-html": "3.3.0",
|
||||||
"luxon": "3.6.1",
|
"luxon": "3.6.1",
|
||||||
"marked": "15.0.12",
|
"marked": "15.0.11",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "4.0.3",
|
"node-vibrant": "4.0.3",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@@ -131,12 +131,13 @@
|
|||||||
"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.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
|
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-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",
|
||||||
"ua-parser-js": "2.0.3",
|
"ua-parser-js": "2.0.3",
|
||||||
"vis-data": "7.1.9",
|
"vis-data": "7.1.9",
|
||||||
|
"vis-network": "9.1.9",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
"weekstart": "2.0.0",
|
"weekstart": "2.0.0",
|
||||||
@@ -149,27 +150,27 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.27.4",
|
"@babel/core": "7.26.10",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||||
"@babel/plugin-transform-runtime": "7.27.4",
|
"@babel/plugin-transform-runtime": "7.26.10",
|
||||||
"@babel/preset-env": "7.27.2",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.20.2",
|
"@bundle-stats/plugin-webpack-filter": "4.19.1",
|
||||||
"@lokalise/node-api": "14.8.0",
|
"@lokalise/node-api": "14.4.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.1",
|
"@octokit/auth-oauth-device": "7.1.5",
|
||||||
"@octokit/plugin-retry": "8.0.1",
|
"@octokit/plugin-retry": "7.2.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "21.1.1",
|
||||||
"@rsdoctor/rspack-plugin": "1.1.2",
|
"@rsdoctor/rspack-plugin": "1.0.2",
|
||||||
"@rspack/cli": "1.3.12",
|
"@rspack/cli": "1.3.8",
|
||||||
"@rspack/core": "1.3.12",
|
"@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.22",
|
"@types/chromecast-caf-receiver": "6.0.21",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.18",
|
"@types/leaflet": "1.9.17",
|
||||||
"@types/leaflet-draw": "1.0.12",
|
"@types/leaflet-draw": "1.0.11",
|
||||||
"@types/leaflet.markercluster": "1.5.5",
|
"@types/leaflet.markercluster": "1.5.5",
|
||||||
"@types/lodash.merge": "4.6.9",
|
"@types/lodash.merge": "4.6.9",
|
||||||
"@types/luxon": "3.6.2",
|
"@types/luxon": "3.6.2",
|
||||||
@@ -179,14 +180,14 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "3.1.4",
|
"@vitest/coverage-v8": "3.1.2",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"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.28.0",
|
"eslint": "9.25.1",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.2",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"eslint-plugin-lit": "2.1.1",
|
"eslint-plugin-lit": "2.1.1",
|
||||||
@@ -196,7 +197,7 @@
|
|||||||
"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",
|
||||||
"gulp": "5.0.1",
|
"gulp": "5.0.0",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
@@ -204,7 +205,7 @@
|
|||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "16.1.0",
|
"lint-staged": "15.5.1",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
@@ -218,9 +219,9 @@
|
|||||||
"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.33.0",
|
"typescript-eslint": "8.31.1",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.1.4",
|
"vitest": "3.1.2",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@@ -232,7 +233,7 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "2.1.0",
|
"@lit/reactive-element": "2.1.0",
|
||||||
"@fullcalendar/daygrid": "6.1.17",
|
"@fullcalendar/daygrid": "6.1.17",
|
||||||
"globals": "16.2.0",
|
"globals": "16.0.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||||
},
|
},
|
||||||
|
@@ -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.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="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="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_2_779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
<mask id="mask0_1110_23734" 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_2_779)">
|
<g mask="url(#mask0_1110_23734)">
|
||||||
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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"/>
|
||||||
<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="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="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="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"/>
|
||||||
<circle cx="39" cy="36" r="34" fill="white"/>
|
<circle cx="47" cy="36" r="34" fill="white"/>
|
||||||
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
|
||||||
<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_1110_23775" 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_2_810)">
|
<g mask="url(#mask0_1110_23775)">
|
||||||
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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"/>
|
||||||
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="31" y="11" 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"/>
|
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<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="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="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"/>
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,18 +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="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="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="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"/>
|
||||||
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
|
||||||
<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"/>
|
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
|
||||||
<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="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"/>
|
||||||
<mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
|
<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="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 id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" 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"/>
|
||||||
</mask>
|
</mask>
|
||||||
<g mask="url(#mask0_2_810)">
|
<g mask="url(#mask0_1180_4965)">
|
||||||
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
|
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
|
||||||
</g>
|
</g>
|
||||||
<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"/>
|
<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"/>
|
||||||
<rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
<rect x="31" y="11" 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"/>
|
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
|
||||||
<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="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="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"/>
|
<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"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -93,8 +93,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
background-color: var(--primary-background-color, #fafafa);
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
.card-content {
|
.card-content {
|
||||||
background: var(
|
background: var(
|
||||||
@@ -151,8 +151,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
|||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: var(--ha-font-size-3xl);
|
font-size: 28px;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
@@ -57,9 +57,9 @@ export class HaPickAuthProvider extends LitElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
h3:before {
|
h3:before {
|
||||||
border-top: 1px solid var(--divider-color);
|
border-top: 1px solid var(--divider-color);
|
||||||
|
@@ -9,7 +9,6 @@ import type { LitElement } from "lit";
|
|||||||
*/
|
*/
|
||||||
export interface DragScrollControllerConfig {
|
export interface DragScrollControllerConfig {
|
||||||
selector: string;
|
selector: string;
|
||||||
enabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DragScrollController implements ReactiveController {
|
export class DragScrollController implements ReactiveController {
|
||||||
@@ -29,47 +28,19 @@ export class DragScrollController implements ReactiveController {
|
|||||||
|
|
||||||
private _scrollContainer?: HTMLElement | null;
|
private _scrollContainer?: HTMLElement | null;
|
||||||
|
|
||||||
private _enabled = true;
|
|
||||||
|
|
||||||
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, enabled }: DragScrollControllerConfig
|
{ selector }: DragScrollControllerConfig
|
||||||
) {
|
) {
|
||||||
this._selector = selector;
|
this._selector = selector;
|
||||||
this._host = host;
|
this._host = host;
|
||||||
this.enabled = enabled ?? true;
|
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
hostUpdated() {
|
hostUpdated() {
|
||||||
if (!this.enabled || this._scrollContainer) {
|
if (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
|
||||||
);
|
);
|
||||||
@@ -78,18 +49,9 @@ export class DragScrollController implements ReactiveController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _detach() {
|
hostDisconnected() {
|
||||||
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 = undefined;
|
|
||||||
}
|
|
||||||
this.scrolled = false;
|
|
||||||
this.scrolling = false;
|
|
||||||
this.mouseIsDown = false;
|
|
||||||
this.scrollStartX = 0;
|
|
||||||
this.scrollLeft = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _mouseDown = (event: MouseEvent) => {
|
private _mouseDown = (event: MouseEvent) => {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
|
import type { InternalPropertyDeclaration } from "lit/decorators";
|
||||||
|
|
||||||
type Callback = (oldValue: any, newValue: any) => void;
|
type Callback = (oldValue: any, newValue: any) => void;
|
||||||
|
|
||||||
@@ -107,6 +108,7 @@ 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;
|
||||||
}) {
|
}) {
|
||||||
@@ -172,7 +174,7 @@ export function storage(options: {
|
|||||||
performUpdate.call(this);
|
performUpdate.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.subscribe) {
|
if (options.state && options.subscribe) {
|
||||||
const connectedCallback = proto.connectedCallback;
|
const connectedCallback = proto.connectedCallback;
|
||||||
const disconnectedCallback = proto.disconnectedCallback;
|
const disconnectedCallback = proto.disconnectedCallback;
|
||||||
|
|
||||||
@@ -190,6 +192,12 @@ 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,4 +1,10 @@
|
|||||||
import type { ReactiveElement, PropertyValues } from "lit";
|
import {
|
||||||
|
ReactiveElement,
|
||||||
|
type PropertyDeclaration,
|
||||||
|
type PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { shallowEqual } from "../util/shallow-equal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform function type.
|
* Transform function type.
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +23,7 @@ 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,
|
||||||
@@ -77,6 +84,11 @@ 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;
|
||||||
|
@@ -1,11 +1,5 @@
|
|||||||
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
|
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
|
||||||
if (
|
if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) {
|
||||||
composedPath.some(
|
|
||||||
(el) =>
|
|
||||||
"tagName" in el &&
|
|
||||||
(el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
import { updateIcon } from "./update_icon";
|
import { updateIcon } from "./update_icon";
|
||||||
import { deviceTrackerIcon } from "./device_tracker_icon";
|
import { deviceTrackerIcon } from "./device_tracker_icon";
|
||||||
|
import { batteryIcon } from "./battery_icon";
|
||||||
|
|
||||||
export const stateIcon = (
|
export const stateIcon = (
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
@@ -9,10 +10,17 @@ export const stateIcon = (
|
|||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
const domain = computeStateDomain(stateObj);
|
const domain = computeStateDomain(stateObj);
|
||||||
const compareState = state ?? stateObj.state;
|
const compareState = state ?? stateObj.state;
|
||||||
|
const dc = stateObj.attributes.device_class;
|
||||||
switch (domain) {
|
switch (domain) {
|
||||||
case "update":
|
case "update":
|
||||||
return updateIcon(stateObj, compareState);
|
return updateIcon(stateObj, compareState);
|
||||||
|
|
||||||
|
case "sensor":
|
||||||
|
if (dc === "battery") {
|
||||||
|
return batteryIcon(stateObj, compareState);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "device_tracker":
|
case "device_tracker":
|
||||||
return deviceTrackerIcon(stateObj, compareState);
|
return deviceTrackerIcon(stateObj, compareState);
|
||||||
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
const validServiceId = /^(\w+)\.(\w+)$/;
|
|
||||||
|
|
||||||
export const isValidServiceId = (actionId: string) =>
|
|
||||||
validServiceId.test(actionId);
|
|
@@ -1,19 +1,9 @@
|
|||||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||||
export const slugify = (value: string, delimiter = "_") => {
|
export const slugify = (value: string, delimiter = "_") => {
|
||||||
const a =
|
const a =
|
||||||
"àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
|
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
|
||||||
const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
|
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
|
||||||
const p = new RegExp(a.split("").join("|"), "g");
|
const p = new RegExp(a.split("").join("|"), "g");
|
||||||
const complex_cyrillic = {
|
|
||||||
ж: "zh",
|
|
||||||
х: "kh",
|
|
||||||
ц: "ts",
|
|
||||||
ч: "ch",
|
|
||||||
ш: "sh",
|
|
||||||
щ: "shch",
|
|
||||||
ю: "iu",
|
|
||||||
я: "ia",
|
|
||||||
};
|
|
||||||
|
|
||||||
let slugified;
|
let slugified;
|
||||||
|
|
||||||
@@ -24,7 +14,6 @@ export const slugify = (value: string, delimiter = "_") => {
|
|||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||||
.replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters
|
|
||||||
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
|
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
|
||||||
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
||||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||||
|
@@ -2,7 +2,7 @@ import type { CSSResult } from "lit";
|
|||||||
|
|
||||||
const _extractCssVars = (
|
const _extractCssVars = (
|
||||||
cssString: string,
|
cssString: string,
|
||||||
condition: (string: string) => boolean = () => true
|
condition: (string) => boolean = () => true
|
||||||
) => {
|
) => {
|
||||||
const variables: Record<string, string> = {};
|
const variables: Record<string, string> = {};
|
||||||
|
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
import { html } from "lit";
|
|
||||||
import type { LocalizeFunc } from "./localize";
|
|
||||||
|
|
||||||
const MARKDOWN_SUPPORT_URL = "https://commonmark.org/help/";
|
|
||||||
|
|
||||||
export const supportsMarkdownHelper = (localize: LocalizeFunc) =>
|
|
||||||
localize("ui.common.supports_markdown", {
|
|
||||||
markdown_help_link: html`<a
|
|
||||||
href=${MARKDOWN_SUPPORT_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>${localize("ui.common.markdown")}</a
|
|
||||||
>`,
|
|
||||||
});
|
|
@@ -1,72 +0,0 @@
|
|||||||
import type { LineSeriesOption } from "echarts";
|
|
||||||
|
|
||||||
export function downSampleLineData(
|
|
||||||
data: LineSeriesOption["data"],
|
|
||||||
chartWidth: number,
|
|
||||||
minX?: number,
|
|
||||||
maxX?: number
|
|
||||||
) {
|
|
||||||
if (!data || data.length < 10) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const width = chartWidth * window.devicePixelRatio;
|
|
||||||
if (data.length <= width) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const min = minX ?? getPointData(data[0]!)[0];
|
|
||||||
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
|
|
||||||
const step = Math.floor((max - min) / width);
|
|
||||||
const frames = new Map<
|
|
||||||
number,
|
|
||||||
{
|
|
||||||
min: { point: (typeof data)[number]; x: number; y: number };
|
|
||||||
max: { point: (typeof data)[number]; x: number; y: number };
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
// Group points into frames
|
|
||||||
for (const point of data) {
|
|
||||||
const pointData = getPointData(point);
|
|
||||||
if (!Array.isArray(pointData)) continue;
|
|
||||||
const x = Number(pointData[0]);
|
|
||||||
const y = Number(pointData[1]);
|
|
||||||
if (isNaN(x) || isNaN(y)) continue;
|
|
||||||
|
|
||||||
const frameIndex = Math.floor((x - min) / step);
|
|
||||||
const frame = frames.get(frameIndex);
|
|
||||||
if (!frame) {
|
|
||||||
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
|
|
||||||
} else {
|
|
||||||
if (frame.min.y > y) {
|
|
||||||
frame.min = { point, x, y };
|
|
||||||
}
|
|
||||||
if (frame.max.y < y) {
|
|
||||||
frame.max = { point, x, y };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert frames back to points
|
|
||||||
const result: typeof data = [];
|
|
||||||
for (const [_i, frame] of frames) {
|
|
||||||
// Use min/max points to preserve visual accuracy
|
|
||||||
// The order of the data must be preserved so max may be before min
|
|
||||||
if (frame.min.x > frame.max.x) {
|
|
||||||
result.push(frame.max.point);
|
|
||||||
}
|
|
||||||
result.push(frame.min.point);
|
|
||||||
if (frame.min.x < frame.max.x) {
|
|
||||||
result.push(frame.max.point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPointData(point: NonNullable<LineSeriesOption["data"]>[number]) {
|
|
||||||
const pointData =
|
|
||||||
point && typeof point === "object" && "value" in point
|
|
||||||
? point.value
|
|
||||||
: point;
|
|
||||||
return pointData as number[];
|
|
||||||
}
|
|
@@ -27,7 +27,6 @@ import "../ha-icon-button";
|
|||||||
import { formatTimeLabel } from "./axis-label";
|
import { formatTimeLabel } from "./axis-label";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import "../chips/ha-assist-chip";
|
import "../chips/ha-assist-chip";
|
||||||
import { downSampleLineData } from "./down-sample";
|
|
||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||||
@@ -49,8 +48,7 @@ export class HaChartBase extends LitElement {
|
|||||||
@property({ attribute: "expand-legend", type: Boolean })
|
@property({ attribute: "expand-legend", type: Boolean })
|
||||||
public expandLegend?: boolean;
|
public expandLegend?: boolean;
|
||||||
|
|
||||||
// extraComponents is not reactive and should not trigger updates
|
@property({ attribute: false }) public extraComponents?: any[];
|
||||||
public extraComponents?: any[];
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@consume({ context: themesContext, subscribe: true })
|
@consume({ context: themesContext, subscribe: true })
|
||||||
@@ -108,49 +106,48 @@ export class HaChartBase extends LitElement {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.options?.dataZoom) {
|
// Add keyboard event listeners
|
||||||
// Add keyboard event listeners
|
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
if (
|
||||||
if (
|
!this._modifierPressed &&
|
||||||
!this._modifierPressed &&
|
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
) {
|
||||||
) {
|
this._modifierPressed = true;
|
||||||
this._modifierPressed = true;
|
if (!this.options?.dataZoom) {
|
||||||
if (!this.options?.dataZoom) {
|
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
|
||||||
}
|
|
||||||
// drag to zoom
|
|
||||||
this.chart?.dispatchAction({
|
|
||||||
type: "takeGlobalCursor",
|
|
||||||
key: "dataZoomSelect",
|
|
||||||
dataZoomSelectActive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
// drag to zoom
|
||||||
|
this.chart?.dispatchAction({
|
||||||
|
type: "takeGlobalCursor",
|
||||||
|
key: "dataZoomSelect",
|
||||||
|
dataZoomSelectActive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyUp = (ev: KeyboardEvent) => {
|
const handleKeyUp = (ev: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
this._modifierPressed &&
|
this._modifierPressed &&
|
||||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||||
) {
|
) {
|
||||||
this._modifierPressed = false;
|
this._modifierPressed = false;
|
||||||
if (!this.options?.dataZoom) {
|
if (!this.options?.dataZoom) {
|
||||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||||
}
|
|
||||||
this.chart?.dispatchAction({
|
|
||||||
type: "takeGlobalCursor",
|
|
||||||
key: "dataZoomSelect",
|
|
||||||
dataZoomSelectActive: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
this.chart?.dispatchAction({
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
type: "takeGlobalCursor",
|
||||||
window.addEventListener("keyup", handleKeyUp);
|
key: "dataZoomSelect",
|
||||||
this._listeners.push(
|
dataZoomSelectActive: false,
|
||||||
() => window.removeEventListener("keydown", handleKeyDown),
|
});
|
||||||
() => window.removeEventListener("keyup", handleKeyUp)
|
}
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
this._listeners.push(
|
||||||
|
() => window.removeEventListener("keydown", handleKeyDown),
|
||||||
|
() => window.removeEventListener("keyup", handleKeyUp)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
@@ -194,19 +191,16 @@ export class HaChartBase extends LitElement {
|
|||||||
<div class="chart"></div>
|
<div class="chart"></div>
|
||||||
</div>
|
</div>
|
||||||
${this._renderLegend()}
|
${this._renderLegend()}
|
||||||
<div class="chart-controls">
|
${this._isZoomed
|
||||||
${this._isZoomed
|
? html`<ha-icon-button
|
||||||
? html`<ha-icon-button
|
class="zoom-reset"
|
||||||
class="zoom-reset"
|
.path=${mdiRestart}
|
||||||
.path=${mdiRestart}
|
@click=${this._handleZoomReset}
|
||||||
@click=${this._handleZoomReset}
|
title=${this.hass.localize(
|
||||||
title=${this.hass.localize(
|
"ui.components.history_charts.zoom_reset"
|
||||||
"ui.components.history_charts.zoom_reset"
|
)}
|
||||||
)}
|
></ha-icon-button>`
|
||||||
></ha-icon-button>`
|
: nothing}
|
||||||
: nothing}
|
|
||||||
<slot name="button"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -216,15 +210,15 @@ export class HaChartBase extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
|
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
|
||||||
if (!legend.show || legend.type !== "custom") {
|
if (!legend.show) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const datasets = ensureArray(this.data);
|
const datasets = ensureArray(this.data);
|
||||||
const items: LegendComponentOption["data"] =
|
const items = (legend.data ||
|
||||||
legend.data ||
|
datasets
|
||||||
((datasets
|
|
||||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||||
.map((d) => d.name ?? d.id) || []) as string[]);
|
.map((d) => d.name ?? d.id) ||
|
||||||
|
[]) as string[];
|
||||||
|
|
||||||
const isMobile = window.matchMedia(
|
const isMobile = window.matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
@@ -239,32 +233,20 @@ export class HaChartBase extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
${items.map((item, index) => {
|
${items.map((item: string, index: number) => {
|
||||||
if (!this.expandLegend && index >= overflowLimit) {
|
if (!this.expandLegend && index >= overflowLimit) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
let itemStyle: Record<string, any> = {};
|
const dataset = datasets.find(
|
||||||
let name = "";
|
(d) => d.id === item || d.name === item
|
||||||
if (typeof item === "string") {
|
);
|
||||||
name = item;
|
const color = dataset?.color as string;
|
||||||
const dataset = datasets.find(
|
const borderColor = dataset?.itemStyle?.borderColor as string;
|
||||||
(d) => d.id === item || d.name === item
|
|
||||||
);
|
|
||||||
itemStyle = {
|
|
||||||
color: dataset?.color as string,
|
|
||||||
...(dataset?.itemStyle as { borderColor?: string }),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
name = item.name ?? "";
|
|
||||||
itemStyle = item.itemStyle ?? {};
|
|
||||||
}
|
|
||||||
const color = itemStyle?.color as string;
|
|
||||||
const borderColor = itemStyle?.borderColor as string;
|
|
||||||
return html`<li
|
return html`<li
|
||||||
.name=${name}
|
.name=${item}
|
||||||
@click=${this._legendClick}
|
@click=${this._legendClick}
|
||||||
class=${classMap({ hidden: this._hiddenDatasets.has(name) })}
|
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
|
||||||
.title=${name}
|
.title=${item}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bullet"
|
class="bullet"
|
||||||
@@ -273,7 +255,7 @@ export class HaChartBase extends LitElement {
|
|||||||
borderColor: borderColor || color,
|
borderColor: borderColor || color,
|
||||||
})}
|
})}
|
||||||
></div>
|
></div>
|
||||||
<div class="label">${name}</div>
|
<div class="label">${item}</div>
|
||||||
</li>`;
|
</li>`;
|
||||||
})}
|
})}
|
||||||
${items.length > overflowLimit
|
${items.length > overflowLimit
|
||||||
@@ -333,9 +315,7 @@ export class HaChartBase extends LitElement {
|
|||||||
this.chart.on("click", (e: ECElementEvent) => {
|
this.chart.on("click", (e: ECElementEvent) => {
|
||||||
fireEvent(this, "chart-click", e);
|
fireEvent(this, "chart-click", e);
|
||||||
});
|
});
|
||||||
if (!this.options?.dataZoom) {
|
this.chart.getZr().on("dblclick", this._handleClickZoom);
|
||||||
this.chart.getZr().on("dblclick", this._handleClickZoom);
|
|
||||||
}
|
|
||||||
if (this._isTouchDevice) {
|
if (this._isTouchDevice) {
|
||||||
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
this.chart.getZr().on("click", (e: ECElementEvent) => {
|
||||||
if (!e.zrByTouch) {
|
if (!e.zrByTouch) {
|
||||||
@@ -400,9 +380,9 @@ export class HaChartBase extends LitElement {
|
|||||||
if (axis.type !== "time" || axis.show === false) {
|
if (axis.type !== "time" || axis.show === false) {
|
||||||
return axis;
|
return axis;
|
||||||
}
|
}
|
||||||
if (axis.min) {
|
if (axis.max && axis.min) {
|
||||||
this._minutesDifference = differenceInMinutes(
|
this._minutesDifference = differenceInMinutes(
|
||||||
(axis.max as Date) || new Date(),
|
axis.max as Date,
|
||||||
axis.min as Date
|
axis.min as Date
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -430,12 +410,6 @@ export class HaChartBase extends LitElement {
|
|||||||
} as XAXisOption;
|
} as XAXisOption;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let legend = this.options?.legend;
|
|
||||||
if (legend) {
|
|
||||||
legend = ensureArray(legend).map((l) =>
|
|
||||||
l.type === "custom" ? { show: false } : l
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const options = {
|
const options = {
|
||||||
animation: !this._reducedMotion,
|
animation: !this._reducedMotion,
|
||||||
darkMode: this._themes.darkMode ?? false,
|
darkMode: this._themes.darkMode ?? false,
|
||||||
@@ -450,7 +424,7 @@ export class HaChartBase extends LitElement {
|
|||||||
iconStyle: { opacity: 0 },
|
iconStyle: { opacity: 0 },
|
||||||
},
|
},
|
||||||
...this.options,
|
...this.options,
|
||||||
legend,
|
legend: { show: false },
|
||||||
xAxis,
|
xAxis,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -494,13 +468,6 @@ export class HaChartBase extends LitElement {
|
|||||||
smooth: false,
|
smooth: false,
|
||||||
},
|
},
|
||||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||||
graph: {
|
|
||||||
label: {
|
|
||||||
color: style.getPropertyValue("--primary-text-color"),
|
|
||||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
|
||||||
textBorderWidth: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
categoryAxis: {
|
categoryAxis: {
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
@@ -633,50 +600,12 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getSeries() {
|
private _getSeries() {
|
||||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
if (!Array.isArray(this.data)) {
|
||||||
| XAXisOption
|
return this.data;
|
||||||
| undefined;
|
}
|
||||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
return this.data.filter(
|
||||||
| YAXisOption
|
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
|
||||||
| undefined;
|
);
|
||||||
const series = ensureArray(this.data)
|
|
||||||
.filter((d) => !this._hiddenDatasets.has(String(d.name ?? d.id)))
|
|
||||||
.map((s) => {
|
|
||||||
if (s.type === "line") {
|
|
||||||
if (yAxis?.type === "log") {
|
|
||||||
// set <=0 values to null so they render as gaps on a log graph
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
data: s.data?.map((v) =>
|
|
||||||
Array.isArray(v)
|
|
||||||
? [
|
|
||||||
v[0],
|
|
||||||
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
|
|
||||||
...v.slice(2),
|
|
||||||
]
|
|
||||||
: v
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (s.sampling === "minmax") {
|
|
||||||
const minX =
|
|
||||||
xAxis?.min && typeof xAxis.min === "number"
|
|
||||||
? xAxis.min
|
|
||||||
: undefined;
|
|
||||||
const maxX =
|
|
||||||
xAxis?.max && typeof xAxis.max === "number"
|
|
||||||
? xAxis.max
|
|
||||||
: undefined;
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
sampling: undefined,
|
|
||||||
data: downSampleLineData(s.data, this.clientWidth, minX, maxX),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
return series;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getDefaultHeight() {
|
private _getDefaultHeight() {
|
||||||
@@ -776,31 +705,21 @@ export class HaChartBase extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.chart-controls {
|
.zoom-reset {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.chart-controls ha-icon-button,
|
|
||||||
.chart-controls ::slotted(ha-icon-button) {
|
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
--mdc-icon-button-size: 32px;
|
--mdc-icon-button-size: 32px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
.chart-controls ha-icon-button.inactive,
|
|
||||||
.chart-controls ::slotted(ha-icon-button.inactive) {
|
|
||||||
color: var(--state-inactive-color);
|
|
||||||
}
|
|
||||||
.chart-legend {
|
.chart-legend {
|
||||||
max-height: 60%;
|
max-height: 60%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px 0 0;
|
padding: 12px 0 0;
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 12px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
.chart-legend ul {
|
.chart-legend ul {
|
||||||
|
@@ -1,299 +0,0 @@
|
|||||||
import type { EChartsType } from "echarts/core";
|
|
||||||
import type { GraphSeriesOption } from "echarts/charts";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state, query } from "lit/decorators";
|
|
||||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
|
||||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
|
||||||
import type { ECOption } from "../../resources/echarts";
|
|
||||||
import "./ha-chart-base";
|
|
||||||
import type { HaChartBase } from "./ha-chart-base";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
|
|
||||||
export interface NetworkNode {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
category?: number;
|
|
||||||
label?: string;
|
|
||||||
value?: number;
|
|
||||||
symbolSize?: number;
|
|
||||||
symbol?: string;
|
|
||||||
itemStyle?: {
|
|
||||||
color?: string;
|
|
||||||
borderColor?: string;
|
|
||||||
borderWidth?: number;
|
|
||||||
};
|
|
||||||
fixed?: boolean;
|
|
||||||
/**
|
|
||||||
* Distance from the center, where 0 is the center and 1 is the edge
|
|
||||||
*/
|
|
||||||
polarDistance?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NetworkLink {
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
value?: number;
|
|
||||||
reverseValue?: number;
|
|
||||||
lineStyle?: {
|
|
||||||
width?: number;
|
|
||||||
color?: string;
|
|
||||||
type?: "solid" | "dashed" | "dotted";
|
|
||||||
};
|
|
||||||
symbolSize?: number | number[];
|
|
||||||
symbol?: string;
|
|
||||||
label?: {
|
|
||||||
show?: boolean;
|
|
||||||
formatter?: string;
|
|
||||||
};
|
|
||||||
ignoreForceLayout?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NetworkData {
|
|
||||||
nodes: NetworkNode[];
|
|
||||||
links: NetworkLink[];
|
|
||||||
categories?: { name: string; symbol: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
|
||||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
|
||||||
|
|
||||||
@customElement("ha-network-graph")
|
|
||||||
export class HaNetworkGraph extends LitElement {
|
|
||||||
public chart?: EChartsType;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public data!: NetworkData;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public tooltipFormatter?: (
|
|
||||||
params: TopLevelFormatterParams
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _reducedMotion = false;
|
|
||||||
|
|
||||||
@state() private _physicsEnabled = true;
|
|
||||||
|
|
||||||
@state() private _showLabels = true;
|
|
||||||
|
|
||||||
private _listeners: (() => void)[] = [];
|
|
||||||
|
|
||||||
private _nodePositions: Record<string, { x: number; y: number }> = {};
|
|
||||||
|
|
||||||
@query("ha-chart-base") private _baseChart?: HaChartBase;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
if (!GraphChart) {
|
|
||||||
import("echarts/lib/chart/graph/install").then((module) => {
|
|
||||||
GraphChart = module;
|
|
||||||
this.requestUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._listeners.push(
|
|
||||||
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
|
|
||||||
if (this._reducedMotion !== matches) {
|
|
||||||
this._reducedMotion = matches;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
while (this._listeners.length) {
|
|
||||||
this._listeners.pop()!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!GraphChart) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
return html`<ha-chart-base
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._getSeries(
|
|
||||||
this.data,
|
|
||||||
this._physicsEnabled,
|
|
||||||
this._reducedMotion,
|
|
||||||
this._showLabels
|
|
||||||
)}
|
|
||||||
.options=${this._createOptions(this.data?.categories)}
|
|
||||||
height="100%"
|
|
||||||
.extraComponents=${[GraphChart]}
|
|
||||||
>
|
|
||||||
<slot name="button" slot="button"></slot>
|
|
||||||
<ha-icon-button
|
|
||||||
slot="button"
|
|
||||||
class=${this._physicsEnabled ? "active" : "inactive"}
|
|
||||||
.path=${mdiGoogleCirclesGroup}
|
|
||||||
@click=${this._togglePhysics}
|
|
||||||
label=${this.hass.localize(
|
|
||||||
"ui.panel.config.common.graph.toggle_physics"
|
|
||||||
)}
|
|
||||||
></ha-icon-button>
|
|
||||||
<ha-icon-button
|
|
||||||
slot="button"
|
|
||||||
class=${this._showLabels ? "active" : "inactive"}
|
|
||||||
.path=${mdiFormatTextVariant}
|
|
||||||
@click=${this._toggleLabels}
|
|
||||||
label=${this.hass.localize(
|
|
||||||
"ui.panel.config.common.graph.toggle_labels"
|
|
||||||
)}
|
|
||||||
></ha-icon-button>
|
|
||||||
</ha-chart-base>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createOptions = memoizeOne(
|
|
||||||
(categories?: NetworkData["categories"]): ECOption => ({
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
confine: true,
|
|
||||||
formatter: this.tooltipFormatter,
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: !!categories?.length,
|
|
||||||
data: categories?.map((category) => ({
|
|
||||||
...category,
|
|
||||||
icon: category.symbol,
|
|
||||||
})),
|
|
||||||
top: 8,
|
|
||||||
},
|
|
||||||
dataZoom: {
|
|
||||||
type: "inside",
|
|
||||||
filterMode: "none",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getSeries = memoizeOne(
|
|
||||||
(
|
|
||||||
data: NetworkData,
|
|
||||||
physicsEnabled: boolean,
|
|
||||||
reducedMotion: boolean,
|
|
||||||
showLabels: boolean
|
|
||||||
) => {
|
|
||||||
const containerWidth = this.clientWidth;
|
|
||||||
const containerHeight = this.clientHeight;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: "network",
|
|
||||||
type: "graph",
|
|
||||||
layout: physicsEnabled ? "force" : "none",
|
|
||||||
draggable: true,
|
|
||||||
roam: true,
|
|
||||||
selectedMode: "single",
|
|
||||||
label: {
|
|
||||||
show: showLabels,
|
|
||||||
position: "right",
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
focus: "adjacency",
|
|
||||||
},
|
|
||||||
force: {
|
|
||||||
repulsion: [400, 600],
|
|
||||||
edgeLength: [200, 300],
|
|
||||||
gravity: 0.1,
|
|
||||||
layoutAnimation: !reducedMotion && data.nodes.length < 100,
|
|
||||||
},
|
|
||||||
edgeSymbol: ["none", "arrow"],
|
|
||||||
edgeSymbolSize: 10,
|
|
||||||
data: data.nodes.map((node) => {
|
|
||||||
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] =
|
|
||||||
{
|
|
||||||
id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
category: node.category,
|
|
||||||
value: node.value,
|
|
||||||
symbolSize: node.symbolSize || 30,
|
|
||||||
symbol: node.symbol || "circle",
|
|
||||||
itemStyle: node.itemStyle || {},
|
|
||||||
fixed: node.fixed,
|
|
||||||
};
|
|
||||||
if (this._nodePositions[node.id]) {
|
|
||||||
echartsNode.x = this._nodePositions[node.id].x;
|
|
||||||
echartsNode.y = this._nodePositions[node.id].y;
|
|
||||||
} else if (typeof node.polarDistance === "number") {
|
|
||||||
// set the position of the node at polarDistance from the center in a random direction
|
|
||||||
const angle = Math.random() * 2 * Math.PI;
|
|
||||||
echartsNode.x =
|
|
||||||
containerWidth / 2 +
|
|
||||||
((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
|
|
||||||
echartsNode.y =
|
|
||||||
containerHeight / 2 +
|
|
||||||
((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
|
|
||||||
this._nodePositions[node.id] = {
|
|
||||||
x: echartsNode.x,
|
|
||||||
y: echartsNode.y,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return echartsNode;
|
|
||||||
}),
|
|
||||||
links: data.links.map((link) => ({
|
|
||||||
...link,
|
|
||||||
value: link.reverseValue
|
|
||||||
? Math.max(link.value ?? 0, link.reverseValue)
|
|
||||||
: link.value,
|
|
||||||
// remove arrow for bidirectional links
|
|
||||||
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
|
|
||||||
})),
|
|
||||||
categories: data.categories || [],
|
|
||||||
},
|
|
||||||
] as any;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _togglePhysics() {
|
|
||||||
if (this._baseChart?.chart) {
|
|
||||||
this._baseChart.chart
|
|
||||||
// @ts-ignore private method but no other way to get the graph positions
|
|
||||||
.getModel()
|
|
||||||
.getSeriesByIndex(0)
|
|
||||||
.getGraph()
|
|
||||||
.eachNode((node: any) => {
|
|
||||||
const layout = node.getLayout();
|
|
||||||
if (layout) {
|
|
||||||
this._nodePositions[node.id] = {
|
|
||||||
x: layout[0],
|
|
||||||
y: layout[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._physicsEnabled = !this._physicsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleLabels() {
|
|
||||||
this._showLabels = !this._showLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-chart-base {
|
|
||||||
height: 100%;
|
|
||||||
--chart-max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon-button,
|
|
||||||
::slotted(ha-icon-button) {
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-network-graph": HaNetworkGraph;
|
|
||||||
}
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"node-selected": { id: string };
|
|
||||||
}
|
|
||||||
}
|
|
@@ -105,41 +105,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))].sort();
|
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
|
||||||
const depthMap = new Map<number, number>();
|
const depthMap = new Map<number, number>();
|
||||||
const sections: Node[][] = [];
|
indexes.sort().forEach((index, i) => {
|
||||||
indexes.forEach((index, i) => {
|
|
||||||
depthMap.set(index, i);
|
depthMap.set(index, i);
|
||||||
const nodesWithIndex = filteredNodes.filter((n) => n.index === index);
|
|
||||||
if (nodesWithIndex.length > 0) {
|
|
||||||
sections.push(
|
|
||||||
sections.length > 0
|
|
||||||
? nodesWithIndex.sort((a, b) => {
|
|
||||||
// sort by the order of their parents in the previous section with orphans at the end
|
|
||||||
const aParentIndex = this._findParentIndex(
|
|
||||||
a.id,
|
|
||||||
data.links,
|
|
||||||
sections
|
|
||||||
);
|
|
||||||
const bParentIndex = this._findParentIndex(
|
|
||||||
b.id,
|
|
||||||
data.links,
|
|
||||||
sections
|
|
||||||
);
|
|
||||||
if (aParentIndex === bParentIndex) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (aParentIndex === -1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (bParentIndex === -1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return aParentIndex - bParentIndex;
|
|
||||||
})
|
|
||||||
: nodesWithIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const links = this._processLinks(filteredNodes, data.links);
|
const links = this._processLinks(filteredNodes, data.links);
|
||||||
const sectionWidth = width / indexes.length;
|
const sectionWidth = width / indexes.length;
|
||||||
@@ -148,7 +117,7 @@ export class HaSankeyChart extends LitElement {
|
|||||||
return {
|
return {
|
||||||
id: "sankey",
|
id: "sankey",
|
||||||
type: "sankey",
|
type: "sankey",
|
||||||
nodes: sections.flat().map((node) => ({
|
nodes: filteredNodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
value: node.value,
|
value: node.value,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
@@ -258,23 +227,6 @@ export class HaSankeyChart extends LitElement {
|
|||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
|
|
||||||
const parent = links.find((l) => l.target === id)?.source;
|
|
||||||
if (!parent) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
let offset = 0;
|
|
||||||
for (let i = sections.length - 1; i >= 0; i--) {
|
|
||||||
const section = sections[i];
|
|
||||||
const index = section.findIndex((n) => n.id === parent);
|
|
||||||
if (index !== -1) {
|
|
||||||
return offset + index;
|
|
||||||
}
|
|
||||||
offset += section.length;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
@@ -82,8 +82,6 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
private _previousYAxisLabelValue = 0;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
@@ -260,11 +258,35 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
margin: 5,
|
margin: 5,
|
||||||
formatter: this._formatYAxisLabel,
|
formatter: (value: number) => {
|
||||||
|
const formatOptions =
|
||||||
|
value >= 1 || value <= -1
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
// show the first significant digit for tiny values
|
||||||
|
maximumFractionDigits: Math.max(
|
||||||
|
2,
|
||||||
|
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const label = formatNumber(
|
||||||
|
value,
|
||||||
|
this.hass.locale,
|
||||||
|
formatOptions
|
||||||
|
);
|
||||||
|
const width = measureTextWidth(label, 12) + 5;
|
||||||
|
if (width > this._yWidth) {
|
||||||
|
this._yWidth = width;
|
||||||
|
fireEvent(this, "y-width-changed", {
|
||||||
|
value: this._yWidth,
|
||||||
|
chartIndex: this.chartIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as YAXisOption,
|
} as YAXisOption,
|
||||||
legend: {
|
legend: {
|
||||||
type: "custom",
|
|
||||||
show: this.showNames,
|
show: this.showNames,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@@ -722,33 +744,6 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _formatYAxisLabel = (value: number) => {
|
|
||||||
const formatOptions =
|
|
||||||
value >= 1 || value <= -1
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
// show the first significant digit for tiny values
|
|
||||||
maximumFractionDigits: Math.max(
|
|
||||||
2,
|
|
||||||
// use the difference to the previous value to determine the number of significant digits #25526
|
|
||||||
-Math.floor(
|
|
||||||
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const label = formatNumber(value, this.hass.locale, formatOptions);
|
|
||||||
const width = measureTextWidth(label, 12) + 5;
|
|
||||||
if (width > this._yWidth) {
|
|
||||||
this._yWidth = width;
|
|
||||||
fireEvent(this, "y-width-changed", {
|
|
||||||
value: this._yWidth,
|
|
||||||
chartIndex: this.chartIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._previousYAxisLabelValue = value;
|
|
||||||
return label;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||||
if (this.logarithmicScale) {
|
if (this.logarithmicScale) {
|
||||||
// log(0) is -Infinity, so we need to set a minimum value
|
// log(0) is -Infinity, so we need to set a minimum value
|
||||||
|
@@ -308,7 +308,6 @@ export class StatisticsChart extends LitElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
type: "custom",
|
|
||||||
show: !this.hideLegend,
|
show: !this.hideLegend,
|
||||||
data: this._legendData,
|
data: this._legendData,
|
||||||
},
|
},
|
||||||
|
@@ -60,7 +60,7 @@ export class HaAssistChip extends AssistChip {
|
|||||||
opacity: var(--ha-assist-chip-active-container-opacity);
|
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@@ -164,8 +164,6 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@state() private _collapsedGroups: string[] = [];
|
@state() private _collapsedGroups: string[] = [];
|
||||||
|
|
||||||
@state() private _lastSelectedRowId: string | null = null;
|
|
||||||
|
|
||||||
private _checkableRowsCount?: number;
|
private _checkableRowsCount?: number;
|
||||||
|
|
||||||
private _checkedRows: string[] = [];
|
private _checkedRows: string[] = [];
|
||||||
@@ -189,7 +187,6 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
public clearSelection(): void {
|
public clearSelection(): void {
|
||||||
this._checkedRows = [];
|
this._checkedRows = [];
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +194,6 @@ export class HaDataTable extends LitElement {
|
|||||||
this._checkedRows = this._filteredData
|
this._checkedRows = this._filteredData
|
||||||
.filter((data) => data.selectable !== false)
|
.filter((data) => data.selectable !== false)
|
||||||
.map((data) => data[this.id]);
|
.map((data) => data[this.id]);
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +207,6 @@ export class HaDataTable extends LitElement {
|
|||||||
this._checkedRows.push(id);
|
this._checkedRows.push(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +217,6 @@ export class HaDataTable extends LitElement {
|
|||||||
this._checkedRows.splice(index, 1);
|
this._checkedRows.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +261,6 @@ export class HaDataTable extends LitElement {
|
|||||||
if (this.columns[columnId].direction) {
|
if (this.columns[columnId].direction) {
|
||||||
this.sortDirection = this.columns[columnId].direction!;
|
this.sortDirection = this.columns[columnId].direction!;
|
||||||
this.sortColumn = columnId;
|
this.sortColumn = columnId;
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
|
|
||||||
fireEvent(this, "sorting-changed", {
|
fireEvent(this, "sorting-changed", {
|
||||||
column: columnId,
|
column: columnId,
|
||||||
@@ -293,7 +286,6 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
if (properties.has("filter")) {
|
if (properties.has("filter")) {
|
||||||
this._debounceSearch(this.filter);
|
this._debounceSearch(this.filter);
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.has("data")) {
|
if (properties.has("data")) {
|
||||||
@@ -304,11 +296,9 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
||||||
this._collapsedGroups = this.initialCollapsedGroups;
|
this._collapsedGroups = this.initialCollapsedGroups;
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||||
} else if (properties.has("groupColumn")) {
|
} else if (properties.has("groupColumn")) {
|
||||||
this._collapsedGroups = [];
|
this._collapsedGroups = [];
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,14 +312,6 @@ export class HaDataTable extends LitElement {
|
|||||||
this._sortFilterData();
|
this._sortFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
properties.has("_filter") ||
|
|
||||||
properties.has("sortColumn") ||
|
|
||||||
properties.has("sortDirection")
|
|
||||||
) {
|
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
||||||
this._filteredData = [...this._filteredData];
|
this._filteredData = [...this._filteredData];
|
||||||
}
|
}
|
||||||
@@ -560,7 +542,7 @@ export class HaDataTable extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
class="mdc-data-table__row-checkbox"
|
class="mdc-data-table__row-checkbox"
|
||||||
@click=${this._handleRowCheckboxClicked}
|
@change=${this._handleRowCheckboxClick}
|
||||||
.rowId=${row[this.id]}
|
.rowId=${row[this.id]}
|
||||||
.disabled=${row.selectable === false}
|
.disabled=${row.selectable === false}
|
||||||
.checked=${this._checkedRows.includes(String(row[this.id]))}
|
.checked=${this._checkedRows.includes(String(row[this.id]))}
|
||||||
@@ -621,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]}`
|
||||||
@@ -740,10 +722,8 @@ export class HaDataTable extends LitElement {
|
|||||||
}, {});
|
}, {});
|
||||||
const groupedItems: DataTableRowData[] = [];
|
const groupedItems: DataTableRowData[] = [];
|
||||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
const collapsed = collapsedGroups.includes(groupName);
|
|
||||||
groupedItems.push({
|
groupedItems.push({
|
||||||
append: true,
|
append: true,
|
||||||
selectable: false,
|
|
||||||
content: html`<div
|
content: html`<div
|
||||||
class="mdc-data-table__cell group-header"
|
class="mdc-data-table__cell group-header"
|
||||||
role="cell"
|
role="cell"
|
||||||
@@ -752,10 +732,9 @@ export class HaDataTable extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.path=${mdiChevronUp}
|
.path=${mdiChevronUp}
|
||||||
.label=${this.hass.localize(
|
class=${collapsedGroups.includes(groupName)
|
||||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
? "collapsed"
|
||||||
)}
|
: ""}
|
||||||
class=${collapsed ? "collapsed" : ""}
|
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
${groupName === UNDEFINED_GROUP_KEY
|
${groupName === UNDEFINED_GROUP_KEY
|
||||||
@@ -771,7 +750,7 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appendRow) {
|
if (appendRow) {
|
||||||
items.push({ append: true, selectable: false, content: appendRow });
|
items.push({ append: true, content: appendRow });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasFab) {
|
if (hasFab) {
|
||||||
@@ -821,84 +800,23 @@ export class HaDataTable extends LitElement {
|
|||||||
this._checkedRows = [];
|
this._checkedRows = [];
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowCheckboxClicked = (ev: Event) => {
|
private _handleRowCheckboxClick = (ev: Event) => {
|
||||||
const checkbox = ev.currentTarget as HaCheckbox;
|
const checkbox = ev.currentTarget as HaCheckbox;
|
||||||
const rowId = (checkbox as any).rowId;
|
const rowId = (checkbox as any).rowId;
|
||||||
|
|
||||||
const groupedData = this._groupData(
|
if (checkbox.checked) {
|
||||||
this._filteredData,
|
if (this._checkedRows.includes(rowId)) {
|
||||||
this.localizeFunc || this.hass.localize,
|
return;
|
||||||
this.appendRow,
|
|
||||||
this.hasFab,
|
|
||||||
this.groupColumn,
|
|
||||||
this.groupOrder,
|
|
||||||
this._collapsedGroups
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
groupedData.find((data) => data[this.id] === rowId)?.selectable === false
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowIndex = groupedData.findIndex((data) => data[this.id] === rowId);
|
|
||||||
|
|
||||||
if (
|
|
||||||
ev instanceof MouseEvent &&
|
|
||||||
ev.shiftKey &&
|
|
||||||
this._lastSelectedRowId !== null
|
|
||||||
) {
|
|
||||||
const lastSelectedRowIndex = groupedData.findIndex(
|
|
||||||
(data) => data[this.id] === this._lastSelectedRowId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastSelectedRowIndex > -1 && rowIndex > -1) {
|
|
||||||
this._checkedRows = [
|
|
||||||
...this._checkedRows,
|
|
||||||
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else if (!checkbox.checked) {
|
|
||||||
if (!this._checkedRows.includes(rowId)) {
|
|
||||||
this._checkedRows = [...this._checkedRows, rowId];
|
|
||||||
}
|
}
|
||||||
|
this._checkedRows = [...this._checkedRows, rowId];
|
||||||
} else {
|
} else {
|
||||||
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
|
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowIndex > -1) {
|
|
||||||
this._lastSelectedRowId = rowId;
|
|
||||||
}
|
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
private _selectRange(
|
|
||||||
groupedData: DataTableRowData[],
|
|
||||||
startIndex: number,
|
|
||||||
endIndex: number
|
|
||||||
) {
|
|
||||||
const start = Math.min(startIndex, endIndex);
|
|
||||||
const end = Math.max(startIndex, endIndex);
|
|
||||||
|
|
||||||
const checkedRows: string[] = [];
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
const row = groupedData[i];
|
|
||||||
if (
|
|
||||||
row &&
|
|
||||||
row.selectable !== false &&
|
|
||||||
!this._checkedRows.includes(row[this.id])
|
|
||||||
) {
|
|
||||||
checkedRows.push(row[this.id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkedRows;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleRowClick = (ev: Event) => {
|
private _handleRowClick = (ev: Event) => {
|
||||||
if (
|
if (
|
||||||
ev
|
ev
|
||||||
@@ -940,7 +858,6 @@ export class HaDataTable extends LitElement {
|
|||||||
if (this.filter) {
|
if (this.filter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
this._debounceSearch(ev.detail.value);
|
this._debounceSearch(ev.detail.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,13 +894,11 @@ export class HaDataTable extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
||||||
}
|
}
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||||
};
|
};
|
||||||
|
|
||||||
public expandAllGroups() {
|
public expandAllGroups() {
|
||||||
this._collapsedGroups = [];
|
this._collapsedGroups = [];
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,7 +916,6 @@ export class HaDataTable extends LitElement {
|
|||||||
delete grouped.undefined;
|
delete grouped.undefined;
|
||||||
}
|
}
|
||||||
this._collapsedGroups = Object.keys(grouped);
|
this._collapsedGroups = Object.keys(grouped);
|
||||||
this._lastSelectedRowId = null;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1014,12 +928,12 @@ export class HaDataTable extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.mdc-data-table__content {
|
.mdc-data-table__content {
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
-webkit-font-smoothing: antialiased;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 1.25rem;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
letter-spacing: 0.0178571429em;
|
letter-spacing: 0.0178571429em;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
@@ -1134,12 +1048,12 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__cell {
|
.mdc-data-table__cell {
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
-webkit-font-smoothing: antialiased;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 1.25rem;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
letter-spacing: 0.0178571429em;
|
letter-spacing: 0.0178571429em;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
@@ -1256,12 +1170,12 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-cell {
|
.mdc-data-table__header-cell {
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: Roboto, sans-serif;
|
||||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
-webkit-font-smoothing: antialiased;
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 0.875rem;
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 1.375rem;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
letter-spacing: 0.0071428571em;
|
letter-spacing: 0.0071428571em;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
text-transform: inherit;
|
text-transform: inherit;
|
||||||
@@ -1285,7 +1199,7 @@ export class HaDataTable extends LitElement {
|
|||||||
padding-inline-start: 12px;
|
padding-inline-start: 12px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@@ -12,7 +12,6 @@ import type { EntityRegistryEntry } from "../../data/entity_registry";
|
|||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-list-item";
|
import "../ha-list-item";
|
||||||
import "../ha-select";
|
import "../ha-select";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
|
||||||
|
|
||||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||||
@@ -104,7 +103,6 @@ export abstract class HaDeviceAutomationPicker<
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.value=${value}
|
.value=${value}
|
||||||
@selected=${this._automationChanged}
|
@selected=${this._automationChanged}
|
||||||
@closed=${stopPropagation}
|
|
||||||
.disabled=${this._automations.length === 0}
|
.disabled=${this._automations.length === 0}
|
||||||
>
|
>
|
||||||
${value === NO_AUTOMATION_KEY
|
${value === NO_AUTOMATION_KEY
|
||||||
|
@@ -1,28 +1,33 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, 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";
|
||||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||||
import {
|
|
||||||
computeDeviceName,
|
|
||||||
computeDeviceNameDisplay,
|
|
||||||
} from "../../common/entity/compute_device_name";
|
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||||
import {
|
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
||||||
getDeviceEntityDisplayLookup,
|
import type {
|
||||||
type DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
type DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { domainToName } from "../../data/integration";
|
import { getDeviceEntityDisplayLookup } from "../../data/device_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-generic-picker";
|
import "../ha-combo-box";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaComboBox } from "../ha-combo-box";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
import "../ha-combo-box-item";
|
||||||
|
|
||||||
|
interface Device {
|
||||||
|
name: string;
|
||||||
|
area: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScorableDevice = ScorableTextItem & Device;
|
||||||
|
|
||||||
export type HaDevicePickerDeviceFilterFunc = (
|
export type HaDevicePickerDeviceFilterFunc = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
@@ -30,35 +35,25 @@ export type HaDevicePickerDeviceFilterFunc = (
|
|||||||
|
|
||||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
interface DevicePickerItem extends PickerComboBoxItem {
|
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
|
||||||
domain?: string;
|
<ha-combo-box-item type="button">
|
||||||
domain_name?: string;
|
<span slot="headline">${item.name}</span>
|
||||||
}
|
${item.area
|
||||||
|
? html`<span slot="supporting-text">${item.area}</span>`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
@customElement("ha-device-picker")
|
@customElement("ha-device-picker")
|
||||||
export class HaDevicePicker extends LitElement {
|
export class HaDevicePicker 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 helper?: string;
|
||||||
|
|
||||||
@property() public placeholder?: string;
|
|
||||||
|
|
||||||
@property({ type: String, attribute: "search-label" })
|
|
||||||
public searchLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show only devices with entities from specific domains.
|
* Show only devices with entities from specific domains.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -97,52 +92,38 @@ export class HaDevicePicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaDevicePickerEntityFilterFunc;
|
public entityFilter?: HaDevicePickerEntityFilterFunc;
|
||||||
|
|
||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ type: Boolean }) public disabled = false;
|
||||||
public hideClearIcon = false;
|
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
super.firstUpdated(_changedProperties);
|
|
||||||
this._loadConfigEntries();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _loadConfigEntries() {
|
private _init = false;
|
||||||
const configEntries = await getConfigEntries(this.hass);
|
|
||||||
this._configEntryLookup = Object.fromEntries(
|
|
||||||
configEntries.map((entry) => [entry.entry_id, entry])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getDevices(
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this._configEntryLookup,
|
|
||||||
this.includeDomains,
|
|
||||||
this.excludeDomains,
|
|
||||||
this.includeDeviceClasses,
|
|
||||||
this.deviceFilter,
|
|
||||||
this.entityFilter,
|
|
||||||
this.excludeDevices
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getDevices = memoizeOne(
|
private _getDevices = memoizeOne(
|
||||||
(
|
(
|
||||||
haDevices: HomeAssistant["devices"],
|
devices: DeviceRegistryEntry[],
|
||||||
haEntities: HomeAssistant["entities"],
|
areas: HomeAssistant["areas"],
|
||||||
configEntryLookup: Record<string, ConfigEntry>,
|
entities: EntityRegistryDisplayEntry[],
|
||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
excludeDevices: this["excludeDevices"]
|
excludeDevices: this["excludeDevices"]
|
||||||
): DevicePickerItem[] => {
|
): ScorableDevice[] => {
|
||||||
const devices = Object.values(haDevices);
|
if (!devices.length) {
|
||||||
const entities = Object.values(haEntities);
|
return [
|
||||||
|
{
|
||||||
|
id: "no_devices",
|
||||||
|
area: "",
|
||||||
|
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
||||||
|
strings: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
|
||||||
@@ -233,158 +214,133 @@ export class HaDevicePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
|
const outputDevices = inputDevices.map((device) => {
|
||||||
const deviceName = computeDeviceNameDisplay(
|
const name = computeDeviceNameDisplay(
|
||||||
device,
|
device,
|
||||||
this.hass,
|
this.hass,
|
||||||
deviceEntityLookup[device.id]
|
deviceEntityLookup[device.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { area } = getDeviceContext(device, this.hass);
|
|
||||||
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
|
||||||
|
|
||||||
const configEntry = device.primary_config_entry
|
|
||||||
? configEntryLookup?.[device.primary_config_entry]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const domain = configEntry?.domain;
|
|
||||||
const domainName = domain
|
|
||||||
? domainToName(this.hass.localize, domain)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
label: "",
|
name:
|
||||||
primary:
|
name ||
|
||||||
deviceName ||
|
|
||||||
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||||
secondary: areaName,
|
area:
|
||||||
domain: configEntry?.domain,
|
device.area_id && areas[device.area_id]
|
||||||
domain_name: domainName,
|
? areas[device.area_id].name
|
||||||
search_labels: [deviceName, areaName, domain, domainName].filter(
|
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||||
Boolean
|
strings: [name || ""],
|
||||||
) as string[],
|
|
||||||
sorting_label: deviceName || "zzz",
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (!outputDevices.length) {
|
||||||
return outputDevices;
|
return [
|
||||||
}
|
{
|
||||||
);
|
id: "no_devices",
|
||||||
|
area: "",
|
||||||
private _valueRenderer = memoizeOne(
|
name: this.hass.localize("ui.components.device-picker.no_match"),
|
||||||
(configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
|
strings: [],
|
||||||
const deviceId = value;
|
},
|
||||||
const device = this.hass.devices[deviceId];
|
];
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
return html`<span slot="headline">${deviceId}</span>`;
|
|
||||||
}
|
}
|
||||||
|
if (outputDevices.length === 1) {
|
||||||
const { area } = getDeviceContext(device, this.hass);
|
return outputDevices;
|
||||||
|
}
|
||||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
return outputDevices.sort((a, b) =>
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
||||||
|
);
|
||||||
const primary = deviceName;
|
|
||||||
const secondary = areaName;
|
|
||||||
|
|
||||||
const configEntry = device.primary_config_entry
|
|
||||||
? configEntriesLookup[device.primary_config_entry]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${configEntry
|
|
||||||
? html`<img
|
|
||||||
slot="start"
|
|
||||||
alt=""
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: configEntry.domain,
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
/>`
|
|
||||||
: nothing}
|
|
||||||
<span slot="headline">${primary}</span>
|
|
||||||
<span slot="supporting-text">${secondary}</span>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
|
||||||
${item.domain
|
|
||||||
? html`
|
|
||||||
<img
|
|
||||||
slot="start"
|
|
||||||
alt=""
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: item.domain,
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes.darkMode,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
|
|
||||||
<span slot="headline">${item.primary}</span>
|
|
||||||
${item.secondary
|
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
|
||||||
: nothing}
|
|
||||||
${item.domain_name
|
|
||||||
? html`
|
|
||||||
<div slot="trailing-supporting-text" class="domain">
|
|
||||||
${item.domain_name}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
const placeholder =
|
|
||||||
this.placeholder ??
|
|
||||||
this.hass.localize("ui.components.device-picker.placeholder");
|
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.device-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-generic-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.autofocus=${this.autofocus}
|
|
||||||
.label=${this.label}
|
|
||||||
.searchLabel=${this.searchLabel}
|
|
||||||
.notFoundLabel=${notFoundLabel}
|
|
||||||
.placeholder=${placeholder}
|
|
||||||
.value=${this.value}
|
|
||||||
.rowRenderer=${this._rowRenderer}
|
|
||||||
.getItems=${this._getItems}
|
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
|
||||||
.valueRenderer=${valueRenderer}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
>
|
|
||||||
</ha-generic-picker>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this.comboBox?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev) {
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const devices = this._getDevices(
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
this.hass.areas,
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.excludeDevices
|
||||||
|
);
|
||||||
|
this.comboBox.items = devices;
|
||||||
|
this.comboBox.filteredItems = devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.device-picker.device")
|
||||||
|
: this.label}
|
||||||
|
.value=${this._value}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
item-id-path="id"
|
||||||
|
item-value-path="id"
|
||||||
|
item-label-path="name"
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._deviceChanged}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
></ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value.toLowerCase();
|
||||||
|
target.filteredItems = filterString.length
|
||||||
|
? fuzzyFilterSort<ScorableDevice>(filterString, target.items || [])
|
||||||
|
: target.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deviceChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
let newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === "no_devices") {
|
||||||
|
newValue = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value });
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||||
import "./ha-device-picker";
|
import "./ha-device-picker";
|
||||||
import type {
|
import type {
|
||||||
HaDevicePickerDeviceFilterFunc,
|
HaDevicePickerDeviceFilterFunc,
|
||||||
|
@@ -4,8 +4,8 @@ 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 HaEntitiesPicker extends LitElement {
|
class HaEntitiesPicker extends LitElement {
|
||||||
@@ -72,7 +72,7 @@ class HaEntitiesPicker extends LitElement {
|
|||||||
public excludeEntities?: string[];
|
public excludeEntities?: 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[];
|
||||||
|
|
||||||
|
539
src/components/entity/ha-entity-combo-box.ts
Normal file
539
src/components/entity/ha-entity-combo-box.ts
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
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 { styleMap } from "lit/directives/style-map";
|
||||||
|
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";
|
||||||
|
|
||||||
|
const FAKE_ENTITY: HassEntity = {
|
||||||
|
entity_id: "",
|
||||||
|
state: "",
|
||||||
|
last_changed: "",
|
||||||
|
last_updated: "",
|
||||||
|
context: { id: "", user_id: null, parent_id: null },
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EntityComboBoxItem extends HassEntity {
|
||||||
|
// Force empty label to always display empty value by default in the search field
|
||||||
|
label: "";
|
||||||
|
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 HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
const CREATE_ID = "___create-new-entity___";
|
||||||
|
const NO_ENTITIES_ID = "___no-entities___";
|
||||||
|
|
||||||
|
const DOMAIN_STYLE = styleMap({
|
||||||
|
fontSize: "var(--ha-font-size-s)",
|
||||||
|
fontWeight: "var(--ha-font-weight-normal)",
|
||||||
|
lineHeight: "var(--ha-line-height-normal)",
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
maxWidth: "30%",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ENTITY_ID_STYLE = styleMap({
|
||||||
|
fontFamily: "var(--ha-font-family-code)",
|
||||||
|
fontSize: "var(--ha-font-size-xs)",
|
||||||
|
});
|
||||||
|
|
||||||
|
@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 }
|
||||||
|
) => 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"]
|
||||||
|
): EntityComboBoxItem[] => {
|
||||||
|
let states: 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 {
|
||||||
|
...FAKE_ENTITY,
|
||||||
|
label: "",
|
||||||
|
entity_id: CREATE_ID + domain,
|
||||||
|
primary: primary,
|
||||||
|
secondary: this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.new_entity"
|
||||||
|
),
|
||||||
|
icon_path: mdiPlus,
|
||||||
|
} satisfies EntityComboBoxItem;
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!entityIds.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...FAKE_ENTITY,
|
||||||
|
label: "",
|
||||||
|
entity_id: NO_ENTITIES_ID,
|
||||||
|
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);
|
||||||
|
|
||||||
|
states = 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 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],
|
||||||
|
label: "",
|
||||||
|
primary: primary,
|
||||||
|
secondary:
|
||||||
|
secondary ||
|
||||||
|
this.hass.localize("ui.components.device-picker.no_area"),
|
||||||
|
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,
|
||||||
|
label: "",
|
||||||
|
entity_id: NO_ENTITIES_ID,
|
||||||
|
primary: 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) {
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
item-value-path="entity_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(
|
||||||
|
[
|
||||||
|
"entity_name",
|
||||||
|
"device_name",
|
||||||
|
"area_name",
|
||||||
|
"translated_domain",
|
||||||
|
"friendly_name", // for backwards compatibility
|
||||||
|
"entity_id", // for technical search
|
||||||
|
],
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
...FAKE_ENTITY,
|
||||||
|
label: "",
|
||||||
|
entity_id: NO_ENTITIES_ID,
|
||||||
|
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,45 +1,27 @@
|
|||||||
import { mdiPlus, mdiShape } 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 type { HassEntity } from "home-assistant-js-websocket";
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
|
||||||
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/context/get_entity_context";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
|
||||||
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 {
|
|
||||||
isHelperDomain,
|
|
||||||
type HelperDomain,
|
|
||||||
} from "../../panels/config/helpers/const";
|
|
||||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
import "../ha-generic-picker";
|
import "../ha-icon-button";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaMdListItem } from "../ha-md-list-item";
|
||||||
import type {
|
|
||||||
PickerComboBoxItem,
|
|
||||||
PickerComboBoxSearchFn,
|
|
||||||
} from "../ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
|
||||||
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";
|
||||||
|
|
||||||
interface EntityComboBoxItem extends PickerComboBoxItem {
|
|
||||||
domain_name?: string;
|
|
||||||
stateObj?: HassEntity;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
|
||||||
|
|
||||||
const CREATE_ID = "___create-new-entity___";
|
|
||||||
|
|
||||||
@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;
|
||||||
@@ -54,9 +36,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
public allowCustomEntity;
|
public allowCustomEntity;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "show-entity-id" })
|
|
||||||
public showEntityId = false;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@@ -65,9 +44,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public placeholder?: string;
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "search-label" })
|
|
||||||
public searchLabel?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,32 +95,50 @@ 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;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@query("#anchor") private _anchor?: HaMdListItem;
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
@query("#input") private _input?: HaEntityComboBox;
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
// Load title translations so it is available when the combo-box opens
|
|
||||||
this.hass.loadBackendTranslation("title");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
@state() private _opened = false;
|
||||||
const entityId = value || "";
|
|
||||||
|
private _renderContent() {
|
||||||
|
const entityId = this.value || "";
|
||||||
|
|
||||||
|
if (!this.value) {
|
||||||
|
return html`
|
||||||
|
<span slot="headline" class="placeholder"
|
||||||
|
>${this.placeholder ??
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.placeholder"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const stateObj = this.hass.states[entityId];
|
const stateObj = this.hass.states[entityId];
|
||||||
|
|
||||||
|
const showClearIcon =
|
||||||
|
!this.required && !this.disabled && !this.hideClearIcon;
|
||||||
|
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return html`
|
return html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||||
slot="start"
|
|
||||||
.path=${mdiShape}
|
|
||||||
style="margin: 0 4px"
|
|
||||||
></ha-svg-icon>
|
|
||||||
<span slot="headline">${entityId}</span>
|
<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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,311 +162,174 @@ export class HaEntityPicker extends LitElement {
|
|||||||
slot="start"
|
slot="start"
|
||||||
></state-badge>
|
></state-badge>
|
||||||
<span slot="headline">${primary}</span>
|
<span slot="headline">${primary}</span>
|
||||||
<span slot="supporting-text">${secondary}</span>
|
<span slot="supporting-text">
|
||||||
|
${secondary ||
|
||||||
|
this.hass.localize("ui.components.device-picker.no_area")}
|
||||||
|
</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>
|
||||||
`;
|
`;
|
||||||
};
|
|
||||||
|
|
||||||
private get _showEntityId() {
|
|
||||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
|
||||||
item,
|
|
||||||
{ index }
|
|
||||||
) => {
|
|
||||||
const showEntityId = this._showEntityId;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
|
||||||
${item.icon_path
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
style="margin: 0 4px"
|
|
||||||
.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" class="domain">
|
|
||||||
${item.domain_name}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getAdditionalItems = () =>
|
|
||||||
this._getCreateItems(this.hass.localize, this.createDomains);
|
|
||||||
|
|
||||||
private _getCreateItems = memoizeOne(
|
|
||||||
(
|
|
||||||
localize: this["hass"]["localize"],
|
|
||||||
createDomains: this["createDomains"]
|
|
||||||
) => {
|
|
||||||
if (!createDomains?.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return createDomains.map((domain) => {
|
|
||||||
const primary = localize(
|
|
||||||
"ui.components.entity.entity-picker.create_helper",
|
|
||||||
{
|
|
||||||
domain: isHelperDomain(domain)
|
|
||||||
? localize(
|
|
||||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
|
||||||
)
|
|
||||||
: domainToName(localize, domain),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: CREATE_ID + domain,
|
|
||||||
primary: primary,
|
|
||||||
secondary: localize("ui.components.entity.entity-picker.new_entity"),
|
|
||||||
icon_path: mdiPlus,
|
|
||||||
} satisfies EntityComboBoxItem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getEntities(
|
|
||||||
this.hass,
|
|
||||||
this.includeDomains,
|
|
||||||
this.excludeDomains,
|
|
||||||
this.entityFilter,
|
|
||||||
this.includeDeviceClasses,
|
|
||||||
this.includeUnitOfMeasurement,
|
|
||||||
this.includeEntities,
|
|
||||||
this.excludeEntities
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getEntities = memoizeOne(
|
|
||||||
(
|
|
||||||
hass: this["hass"],
|
|
||||||
includeDomains: this["includeDomains"],
|
|
||||||
excludeDomains: this["excludeDomains"],
|
|
||||||
entityFilter: this["entityFilter"],
|
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
|
||||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
|
||||||
includeEntities: this["includeEntities"],
|
|
||||||
excludeEntities: this["excludeEntities"]
|
|
||||||
): EntityComboBoxItem[] => {
|
|
||||||
let items: EntityComboBoxItem[] = [];
|
|
||||||
|
|
||||||
let entityIds = Object.keys(hass.states);
|
|
||||||
|
|
||||||
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 ? " ◂ " : " ▸ ");
|
|
||||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: entityId,
|
|
||||||
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[],
|
|
||||||
a11y_label: a11yLabel,
|
|
||||||
stateObj: stateObj,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const placeholder =
|
|
||||||
this.placeholder ??
|
|
||||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
.hass=${this.hass}
|
<div class="container">
|
||||||
.disabled=${this.disabled}
|
${!this._opened
|
||||||
.autofocus=${this.autofocus}
|
? html`<ha-combo-box-item
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.disabled=${this.disabled}
|
||||||
.label=${this.label}
|
id="anchor"
|
||||||
.helper=${this.helper}
|
type="button"
|
||||||
.searchLabel=${this.searchLabel}
|
compact
|
||||||
.notFoundLabel=${notFoundLabel}
|
@click=${this._showPicker}
|
||||||
.placeholder=${placeholder}
|
>
|
||||||
.value=${this.value}
|
${this._renderContent()}
|
||||||
.rowRenderer=${this._rowRenderer}
|
</ha-combo-box-item>`
|
||||||
.getItems=${this._getItems}
|
: html`<ha-entity-combo-box
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
id="input"
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hass=${this.hass}
|
||||||
.searchFn=${this._searchFn}
|
.autofocus=${this.autofocus}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.allowCustomEntity=${this.allowCustomEntity}
|
||||||
@value-changed=${this._valueChanged}
|
.label=${this.hass.localize("ui.common.search")}
|
||||||
>
|
.value=${this.value}
|
||||||
</ha-generic-picker>
|
.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 _searchFn: PickerComboBoxSearchFn<EntityComboBoxItem> = (
|
private _renderHelper() {
|
||||||
search,
|
return this.helper
|
||||||
filteredItems
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
) => {
|
: nothing;
|
||||||
// If there is exact match for entity id, put it first
|
|
||||||
const index = filteredItems.findIndex(
|
|
||||||
(item) => item.stateObj?.entity_id === search
|
|
||||||
);
|
|
||||||
if (index === -1) {
|
|
||||||
return filteredItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [exactMatch] = filteredItems.splice(index, 1);
|
|
||||||
filteredItems.unshift(exactMatch);
|
|
||||||
return filteredItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
public async open() {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._picker?.open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev) {
|
private _clear(e) {
|
||||||
ev.stopPropagation();
|
e.stopPropagation();
|
||||||
const value = ev.detail.value;
|
this.value = undefined;
|
||||||
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
if (!value) {
|
|
||||||
this._setValue(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.startsWith(CREATE_ID)) {
|
|
||||||
const domain = value.substring(CREATE_ID.length);
|
|
||||||
|
|
||||||
showHelperDetailDialog(this, {
|
|
||||||
domain,
|
|
||||||
dialogClosedCallback: (item) => {
|
|
||||||
if (item.entityId) this._setValue(item.entityId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidEntityId(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setValue(value: string | undefined) {
|
|
||||||
this.value = value;
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value });
|
|
||||||
fireEvent(this, "change");
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
mwc-menu-surface {
|
||||||
|
--mdc-menu-min-width: 100%;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -267,7 +267,7 @@ export class HaStateLabelBadge extends LitElement {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.big {
|
.big {
|
||||||
font-size: var(--ha-font-size-xs);
|
font-size: 70%;
|
||||||
}
|
}
|
||||||
ha-label-badge {
|
ha-label-badge {
|
||||||
--ha-label-badge-color: var(--label-badge-red);
|
--ha-label-badge-color: var(--label-badge-red);
|
||||||
|
482
src/components/entity/ha-statistic-combo-box.ts
Normal file
482
src/components/entity/ha-statistic-combo-box.ts
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import { mdiChartLine, 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 { styleMap } from "lit/directives/style-map";
|
||||||
|
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";
|
||||||
|
|
||||||
|
type StatisticItemType = "entity" | "external" | "no_state";
|
||||||
|
|
||||||
|
interface StatisticItem {
|
||||||
|
id: string;
|
||||||
|
label: "";
|
||||||
|
primary: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
||||||
|
|
||||||
|
const ENTITY_ID_STYLE = styleMap({
|
||||||
|
fontFamily: "var(--ha-font-family-code)",
|
||||||
|
fontSize: "11px",
|
||||||
|
});
|
||||||
|
|
||||||
|
@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 }
|
||||||
|
) => 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: "",
|
||||||
|
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,
|
||||||
|
iconPath: mdiShape,
|
||||||
|
});
|
||||||
|
} 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(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,
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
label: "",
|
||||||
|
state: stateObj,
|
||||||
|
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: "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
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") {
|
||||||
|
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(
|
||||||
|
[
|
||||||
|
"entity_name",
|
||||||
|
"device_name",
|
||||||
|
"area_name",
|
||||||
|
"friendly_name", // for backwards compatibility
|
||||||
|
"id", // for technical search
|
||||||
|
],
|
||||||
|
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,48 +1,45 @@
|
|||||||
import { mdiChartLine, mdiHelpCircle, mdiShape } 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 type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import {
|
||||||
import { customElement, property, query } from "lit/decorators";
|
css,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
type CSSResultGroup,
|
||||||
|
type PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
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/context/get_entity_context";
|
||||||
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 {
|
import {
|
||||||
getStatisticIds,
|
getStatisticIds,
|
||||||
getStatisticLabel,
|
getStatisticLabel,
|
||||||
type StatisticsMetaData,
|
type StatisticsMetaData,
|
||||||
} from "../../data/recorder";
|
} from "../../data/recorder";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
|
||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
import "../ha-generic-picker";
|
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-input-helper-text";
|
import type { HaMdListItem } from "../ha-md-list-item";
|
||||||
import type {
|
|
||||||
PickerComboBoxItem,
|
|
||||||
PickerComboBoxSearchFn,
|
|
||||||
} from "../ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
|
||||||
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";
|
||||||
|
|
||||||
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
|
interface StatisticItem {
|
||||||
|
primary: string;
|
||||||
const MISSING_ID = "___missing-entity___";
|
secondary?: string;
|
||||||
|
iconPath?: string;
|
||||||
type StatisticItemType = "entity" | "external" | "no_state";
|
|
||||||
|
|
||||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
|
||||||
statistic_id?: string;
|
|
||||||
stateObj?: HassEntity;
|
stateObj?: HassEntity;
|
||||||
type?: StatisticItemType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-statistic-picker")
|
@customElement("ha-statistic-picker")
|
||||||
@@ -73,9 +70,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
public statisticIds?: StatisticsMetaData[];
|
public statisticIds?: StatisticsMetaData[];
|
||||||
|
|
||||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
|
||||||
"/more-info/statistics/";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show only statistics natively stored with these units of measurements.
|
* Show only statistics natively stored with these units of measurements.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -120,7 +114,11 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@query("#anchor") private _anchor?: HaMdListItem;
|
||||||
|
|
||||||
|
@query("#input") private _input?: HaEntityComboBox;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
if (
|
if (
|
||||||
@@ -135,167 +133,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getStatisticsItems(
|
|
||||||
this.hass,
|
|
||||||
this.statisticIds,
|
|
||||||
this.includeStatisticsUnitOfMeasurement,
|
|
||||||
this.includeUnitClass,
|
|
||||||
this.includeDeviceClass,
|
|
||||||
this.entitiesOnly,
|
|
||||||
this.excludeStatistics,
|
|
||||||
this.value
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getAdditionalItems(): StatisticComboBoxItem[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: MISSING_ID,
|
|
||||||
primary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.missing_entity"
|
|
||||||
),
|
|
||||||
icon_path: mdiHelpCircle,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getStatisticsItems = memoizeOne(
|
|
||||||
(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
statisticIds?: StatisticsMetaData[],
|
|
||||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
|
||||||
includeUnitClass?: string | string[],
|
|
||||||
includeDeviceClass?: string | string[],
|
|
||||||
entitiesOnly?: boolean,
|
|
||||||
excludeStatistics?: string[],
|
|
||||||
value?: string
|
|
||||||
): StatisticComboBoxItem[] => {
|
|
||||||
if (!statisticIds) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
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: StatisticComboBoxItem[] = [];
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf(type)}`;
|
|
||||||
if (type === "no_state") {
|
|
||||||
output.push({
|
|
||||||
id,
|
|
||||||
primary: label,
|
|
||||||
secondary: this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_state"
|
|
||||||
),
|
|
||||||
type,
|
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
|
||||||
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,
|
|
||||||
type,
|
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
|
||||||
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 ? " ◂ " : " ▸ ");
|
|
||||||
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
|
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
|
||||||
output.push({
|
|
||||||
id,
|
|
||||||
statistic_id: id,
|
|
||||||
primary,
|
|
||||||
secondary,
|
|
||||||
a11y_label: a11yLabel,
|
|
||||||
stateObj: stateObj,
|
|
||||||
type: "entity",
|
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
|
||||||
search_labels: [
|
|
||||||
entityName,
|
|
||||||
deviceName,
|
|
||||||
areaName,
|
|
||||||
friendlyName,
|
|
||||||
id,
|
|
||||||
].filter(Boolean) as string[],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _statisticMetaData = memoizeOne(
|
private _statisticMetaData = memoizeOne(
|
||||||
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
|
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
|
||||||
if (!statisticIds) {
|
if (!statisticIds) {
|
||||||
@@ -307,11 +144,26 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
private _renderContent() {
|
||||||
const statisticId = value;
|
const statisticId = this.value || "";
|
||||||
|
|
||||||
|
if (!this.value) {
|
||||||
|
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 item = this._computeItem(statisticId);
|
const item = this._computeItem(statisticId);
|
||||||
|
|
||||||
|
const showClearIcon =
|
||||||
|
!this.required && !this.disabled && !this.hideClearIcon;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${item.stateObj
|
${item.stateObj
|
||||||
? html`
|
? html`
|
||||||
@@ -321,19 +173,29 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
slot="start"
|
slot="start"
|
||||||
></state-badge>
|
></state-badge>
|
||||||
`
|
`
|
||||||
: item.icon_path
|
: item.iconPath
|
||||||
? html`
|
? html`<ha-svg-icon
|
||||||
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
|
slot="start"
|
||||||
`
|
.path=${item.iconPath}
|
||||||
|
></ha-svg-icon>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<span slot="headline">${item.primary}</span>
|
<span slot="headline">${item.primary}</span>
|
||||||
${item.secondary
|
${item.secondary
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
: nothing}
|
: 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): StatisticComboBoxItem {
|
private _computeItem(statisticId: string): StatisticItem {
|
||||||
const stateObj = this.hass.states[statisticId];
|
const stateObj = this.hass.states[statisticId];
|
||||||
|
|
||||||
if (stateObj) {
|
if (stateObj) {
|
||||||
@@ -349,24 +211,11 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isRTL ? " ◂ " : " ▸ ");
|
.join(isRTL ? " ◂ " : " ▸ ");
|
||||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
|
|
||||||
return {
|
return {
|
||||||
id: statisticId,
|
|
||||||
statistic_id: statisticId,
|
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
stateObj: stateObj,
|
stateObj,
|
||||||
type: "entity",
|
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
|
||||||
search_labels: [
|
|
||||||
entityName,
|
|
||||||
deviceName,
|
|
||||||
areaName,
|
|
||||||
friendlyName,
|
|
||||||
statisticId,
|
|
||||||
].filter(Boolean) as string[],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,143 +230,175 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
: "no_state";
|
: "no_state";
|
||||||
|
|
||||||
if (type === "external") {
|
if (type === "external") {
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
|
|
||||||
const label = getStatisticLabel(this.hass, statisticId, statistic);
|
const label = getStatisticLabel(this.hass, statisticId, statistic);
|
||||||
const domain = statisticId.split(":")[0];
|
const domain = statisticId.split(":")[0];
|
||||||
const domainName = domainToName(this.hass.localize, domain);
|
const domainName = domainToName(this.hass.localize, domain);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: statisticId,
|
|
||||||
statistic_id: statisticId,
|
|
||||||
primary: label,
|
primary: label,
|
||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type: "external",
|
iconPath: mdiChartLine,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
|
||||||
search_labels: [label, domainName, statisticId],
|
|
||||||
icon_path: mdiChartLine,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
|
|
||||||
const label = getStatisticLabel(this.hass, statisticId, statistic);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: statisticId,
|
primary: statisticId,
|
||||||
primary: label,
|
iconPath: mdiShape,
|
||||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
|
||||||
type: "no_state",
|
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
|
||||||
search_labels: [label, statisticId],
|
|
||||||
icon_path: mdiShape,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
|
|
||||||
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 || item.type
|
|
||||||
? html`<span slot="supporting-text"
|
|
||||||
>${item.secondary} - ${item.type}</span
|
|
||||||
>`
|
|
||||||
: nothing}
|
|
||||||
${item.statistic_id && showEntityId
|
|
||||||
? html`<span slot="supporting-text" class="code">
|
|
||||||
${item.statistic_id}
|
|
||||||
</span>`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const placeholder =
|
|
||||||
this.placeholder ??
|
|
||||||
this.hass.localize("ui.components.statistic-picker.placeholder");
|
|
||||||
const notFoundLabel = this.hass.localize(
|
|
||||||
"ui.components.statistic-picker.no_match"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
.hass=${this.hass}
|
<div class="container">
|
||||||
.autofocus=${this.autofocus}
|
${!this._opened
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
? html`
|
||||||
.label=${this.label}
|
<ha-combo-box-item
|
||||||
.notFoundLabel=${notFoundLabel}
|
.disabled=${this.disabled}
|
||||||
.placeholder=${placeholder}
|
id="anchor"
|
||||||
.value=${this.value}
|
type="button"
|
||||||
.rowRenderer=${this._rowRenderer}
|
compact
|
||||||
.getItems=${this._getItems}
|
@click=${this._showPicker}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
>
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
${this._renderContent()}
|
||||||
.searchFn=${this._searchFn}
|
</ha-combo-box-item>
|
||||||
.valueRenderer=${this._valueRenderer}
|
`
|
||||||
@value-changed=${this._valueChanged}
|
: html`
|
||||||
>
|
<ha-statistic-combo-box
|
||||||
</ha-generic-picker>
|
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 _searchFn: PickerComboBoxSearchFn<StatisticComboBoxItem> = (
|
private _renderHelper() {
|
||||||
search,
|
return this.helper
|
||||||
filteredItems
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
) => {
|
: nothing;
|
||||||
// If there is exact match for entity id or statistic id, put it first
|
}
|
||||||
const index = filteredItems.findIndex(
|
|
||||||
(item) =>
|
|
||||||
item.stateObj?.entity_id === search || item.statistic_id === search
|
|
||||||
);
|
|
||||||
if (index === -1) {
|
|
||||||
return filteredItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [exactMatch] = filteredItems.splice(index, 1);
|
private _clear(e) {
|
||||||
filteredItems.unshift(exactMatch);
|
e.stopPropagation();
|
||||||
return filteredItems;
|
this.value = undefined;
|
||||||
};
|
fireEvent(this, "value-changed", { value: undefined });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private async _showPicker() {
|
||||||
ev.stopPropagation();
|
if (this.disabled) {
|
||||||
const value = ev.detail.value;
|
|
||||||
|
|
||||||
if (value === MISSING_ID) {
|
|
||||||
window.open(
|
|
||||||
documentationUrl(this.hass, this.helpMissingEntityUrl),
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._opened = true;
|
||||||
this.value = value;
|
await this.updateComplete;
|
||||||
fireEvent(this, "value-changed", { value });
|
this._input?.focus();
|
||||||
|
this._input?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
// Multiple calls to _openedChanged can be triggered in quick succession
|
||||||
await this.updateComplete;
|
// when the menu is opened
|
||||||
await this._picker?.open();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ class StateInfo extends LitElement {
|
|||||||
|
|
||||||
.name.in-dialog,
|
.name.in-dialog,
|
||||||
:host([secondary-line]) .name {
|
:host([secondary-line]) .name {
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-ago,
|
.time-ago,
|
||||||
|
@@ -129,7 +129,7 @@ class HaAlert extends LitElement {
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.action mwc-button,
|
.action mwc-button,
|
||||||
.action ha-icon-button {
|
.action ha-icon-button {
|
||||||
|
@@ -56,7 +56,7 @@ export class HaAnsiToHtml extends LitElement {
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.italic {
|
.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import { mdiTextureBox } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { TemplateResult } from "lit";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
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 { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import type { ScorableTextItem } from "../common/string/filter/sequence-matching";
|
||||||
|
import { fuzzyFilterSort } from "../common/string/filter/sequence-matching";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import type { AreaRegistryEntry } from "../data/area_registry";
|
import type { AreaRegistryEntry } from "../data/area_registry";
|
||||||
import type {
|
import type {
|
||||||
@@ -19,33 +19,29 @@ import type {
|
|||||||
} from "../data/device_registry";
|
} from "../data/device_registry";
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import {
|
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||||
getFloorAreaLookup,
|
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||||
type FloorRegistryEntry,
|
|
||||||
} from "../data/floor_registry";
|
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-floor-icon";
|
import "./ha-floor-icon";
|
||||||
import "./ha-generic-picker";
|
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-tree-indicator";
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
const SEPARATOR = "________";
|
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
||||||
|
|
||||||
interface FloorComboBoxItem extends PickerComboBoxItem {
|
interface FloorAreaEntry {
|
||||||
type: "floor" | "area";
|
id: string | null;
|
||||||
floor?: FloorRegistryEntry;
|
name: string;
|
||||||
area?: AreaRegistryEntry;
|
icon: string | null;
|
||||||
}
|
strings: string[];
|
||||||
|
|
||||||
interface AreaFloorValue {
|
|
||||||
id: string;
|
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
|
level: number | null;
|
||||||
|
hasFloor?: boolean;
|
||||||
|
lastArea?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-area-floor-picker")
|
@customElement("ha-area-floor-picker")
|
||||||
@@ -54,15 +50,12 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: AreaFloorValue;
|
@property() public value?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@property() public placeholder?: string;
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "search-label" })
|
|
||||||
public searchLabel?: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show only areas with entities from specific domains.
|
* Show only areas with entities from specific domains.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -113,53 +106,66 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this.comboBox?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
public async focus() {
|
||||||
const item = this._parseValue(value);
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
const area = item.type === "area" && this.hass.areas[value];
|
}
|
||||||
|
|
||||||
if (area) {
|
|
||||||
const areaName = computeAreaName(area);
|
|
||||||
return html`
|
|
||||||
${area.icon
|
|
||||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${mdiTextureBox}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
<slot name="headline">${areaName}</slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const floor = item.type === "floor" && this.hass.floors[value];
|
|
||||||
|
|
||||||
if (floor) {
|
|
||||||
const floorName = computeFloorName(floor);
|
|
||||||
return html`
|
|
||||||
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
|
|
||||||
<span slot="headline">${floorName}</span>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
|
||||||
|
const rtl = computeRTL(this.hass);
|
||||||
return html`
|
return html`
|
||||||
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
|
<ha-combo-box-item
|
||||||
<span slot="headline">${value}</span>
|
type="button"
|
||||||
|
style=${item.type === "area" && item.hasFloor
|
||||||
|
? "--md-list-item-leading-space: 48px;"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${item.type === "area" && item.hasFloor
|
||||||
|
? html`
|
||||||
|
<ha-tree-indicator
|
||||||
|
style=${styleMap({
|
||||||
|
width: "48px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "0px",
|
||||||
|
left: rtl ? undefined : "4px",
|
||||||
|
right: rtl ? "4px" : undefined,
|
||||||
|
transform: rtl ? "scaleX(-1)" : "",
|
||||||
|
})}
|
||||||
|
.end=${item.lastArea}
|
||||||
|
slot="start"
|
||||||
|
></ha-tree-indicator>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${item.type === "floor"
|
||||||
|
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
|
||||||
|
: item.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${item.name}
|
||||||
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getAreasAndFloors = memoizeOne(
|
private _getAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
haFloors: HomeAssistant["floors"],
|
floors: FloorRegistryEntry[],
|
||||||
haAreas: HomeAssistant["areas"],
|
areas: AreaRegistryEntry[],
|
||||||
haDevices: HomeAssistant["devices"],
|
devices: DeviceRegistryEntry[],
|
||||||
haEntities: HomeAssistant["entities"],
|
entities: EntityRegistryDisplayEntry[],
|
||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
@@ -167,11 +173,19 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
excludeAreas: this["excludeAreas"],
|
excludeAreas: this["excludeAreas"],
|
||||||
excludeFloors: this["excludeFloors"]
|
excludeFloors: this["excludeFloors"]
|
||||||
): FloorComboBoxItem[] => {
|
): FloorAreaEntry[] => {
|
||||||
const floors = Object.values(haFloors);
|
if (!areas.length && !floors.length) {
|
||||||
const areas = Object.values(haAreas);
|
return [
|
||||||
const devices = Object.values(haDevices);
|
{
|
||||||
const entities = Object.values(haEntities);
|
id: "no_areas",
|
||||||
|
type: "area",
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||||
|
icon: null,
|
||||||
|
strings: [],
|
||||||
|
level: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
@@ -312,6 +326,19 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!outputAreas.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "no_areas",
|
||||||
|
type: "area",
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
strings: [],
|
||||||
|
level: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||||
const unassisgnedAreas = Object.values(outputAreas).filter(
|
const unassisgnedAreas = Object.values(outputAreas).filter(
|
||||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
@@ -333,186 +360,151 @@ export class HaAreaFloorPicker extends LitElement {
|
|||||||
return stringCompare(floorA.name, floorB.name);
|
return stringCompare(floorA.name, floorB.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
const items: FloorComboBoxItem[] = [];
|
const output: FloorAreaEntry[] = [];
|
||||||
|
|
||||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
if (floor) {
|
if (floor) {
|
||||||
const floorName = computeFloorName(floor);
|
output.push({
|
||||||
|
id: floor.floor_id,
|
||||||
const areaSearchLabels = floorAreas
|
|
||||||
.map((area) => {
|
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
|
||||||
return [area.area_id, areaName, ...area.aliases];
|
|
||||||
})
|
|
||||||
.flat();
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
id: this._formatValue({ id: floor.floor_id, type: "floor" }),
|
|
||||||
type: "floor",
|
type: "floor",
|
||||||
primary: floorName,
|
name: floor.name,
|
||||||
floor: floor,
|
icon: floor.icon,
|
||||||
search_labels: [
|
strings: [floor.floor_id, ...floor.aliases, floor.name],
|
||||||
floor.floor_id,
|
level: floor.level,
|
||||||
floorName,
|
|
||||||
...floor.aliases,
|
|
||||||
...areaSearchLabels,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
items.push(
|
output.push(
|
||||||
...floorAreas.map((area) => {
|
...floorAreas.map((area, index, array) => ({
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
id: area.area_id,
|
||||||
return {
|
type: "area" as const,
|
||||||
id: this._formatValue({ id: area.area_id, type: "area" }),
|
name: area.name,
|
||||||
type: "area" as const,
|
icon: area.icon,
|
||||||
primary: areaName,
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
area: area,
|
hasFloor: true,
|
||||||
icon: area.icon || undefined,
|
level: null,
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
lastArea: index === array.length - 1,
|
||||||
};
|
}))
|
||||||
})
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push(
|
if (!output.length && !unassisgnedAreas.length) {
|
||||||
...unassisgnedAreas.map((area) => {
|
output.push({
|
||||||
const areaName = computeAreaName(area) || area.area_id;
|
id: "no_areas",
|
||||||
return {
|
type: "area",
|
||||||
id: this._formatValue({ id: area.area_id, type: "area" }),
|
name: this.hass.localize(
|
||||||
type: "area" as const,
|
"ui.components.area-picker.unassigned_areas"
|
||||||
primary: areaName,
|
),
|
||||||
icon: area.icon || undefined,
|
icon: null,
|
||||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
strings: [],
|
||||||
};
|
level: null,
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
...unassisgnedAreas.map((area) => ({
|
||||||
|
id: area.area_id,
|
||||||
|
type: "area" as const,
|
||||||
|
name: area.name,
|
||||||
|
icon: area.icon,
|
||||||
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
level: null,
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
return items;
|
return output;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
protected updated(changedProps: PropertyValues) {
|
||||||
item,
|
if (
|
||||||
{ index },
|
(!this._init && this.hass) ||
|
||||||
combobox
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
) => {
|
) {
|
||||||
const nextItem = combobox.filteredItems?.[index + 1];
|
this._init = true;
|
||||||
const isLastArea =
|
const areas = this._getAreas(
|
||||||
!nextItem ||
|
Object.values(this.hass.floors),
|
||||||
nextItem.type === "floor" ||
|
Object.values(this.hass.areas),
|
||||||
(nextItem.type === "area" && !nextItem.area?.floor_id);
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
const rtl = computeRTL(this.hass);
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
const hasFloor = item.type === "area" && item.area?.floor_id;
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
return html`
|
this.entityFilter,
|
||||||
<ha-combo-box-item
|
this.excludeAreas,
|
||||||
type="button"
|
this.excludeFloors
|
||||||
style=${item.type === "area" && hasFloor
|
);
|
||||||
? "--md-list-item-leading-space: 48px;"
|
this.comboBox.items = areas;
|
||||||
: ""}
|
this.comboBox.filteredItems = areas;
|
||||||
>
|
}
|
||||||
${item.type === "area" && hasFloor
|
}
|
||||||
? html`
|
|
||||||
<ha-tree-indicator
|
|
||||||
style=${styleMap({
|
|
||||||
width: "48px",
|
|
||||||
position: "absolute",
|
|
||||||
top: "0px",
|
|
||||||
left: rtl ? undefined : "4px",
|
|
||||||
right: rtl ? "4px" : undefined,
|
|
||||||
transform: rtl ? "scaleX(-1)" : "",
|
|
||||||
})}
|
|
||||||
.end=${isLastArea}
|
|
||||||
slot="start"
|
|
||||||
></ha-tree-indicator>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${item.type === "floor" && item.floor
|
|
||||||
? html`<ha-floor-icon
|
|
||||||
slot="start"
|
|
||||||
.floor=${item.floor}
|
|
||||||
></ha-floor-icon>`
|
|
||||||
: item.icon
|
|
||||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${item.icon_path || mdiTextureBox}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
${item.primary}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getItems = () =>
|
|
||||||
this._getAreasAndFloors(
|
|
||||||
this.hass.floors,
|
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this.includeDomains,
|
|
||||||
this.excludeDomains,
|
|
||||||
this.includeDeviceClasses,
|
|
||||||
this.deviceFilter,
|
|
||||||
this.entityFilter,
|
|
||||||
this.excludeAreas,
|
|
||||||
this.excludeFloors
|
|
||||||
);
|
|
||||||
|
|
||||||
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
|
|
||||||
[value.type, value.id].join(SEPARATOR)
|
|
||||||
);
|
|
||||||
|
|
||||||
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
|
|
||||||
const [type, id] = value.split(SEPARATOR);
|
|
||||||
|
|
||||||
return { id, type: type as "floor" | "area" };
|
|
||||||
});
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const placeholder =
|
|
||||||
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
|
||||||
|
|
||||||
const value = this.value ? this._formatValue(this.value) : undefined;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.helper=${this.helper}
|
||||||
.label=${this.label}
|
item-value-path="id"
|
||||||
.searchLabel=${this.searchLabel}
|
item-id-path="id"
|
||||||
.notFoundLabel=${this.hass.localize(
|
item-label-path="name"
|
||||||
"ui.components.area-picker.no_match"
|
.value=${this._value}
|
||||||
)}
|
.disabled=${this.disabled}
|
||||||
.placeholder=${placeholder}
|
.required=${this.required}
|
||||||
.value=${value}
|
.label=${this.label === undefined && this.hass
|
||||||
.getItems=${this._getItems}
|
? this.hass.localize("ui.components.area-picker.area")
|
||||||
.valueRenderer=${this._valueRenderer}
|
: this.label}
|
||||||
.rowRenderer=${this._rowRenderer}
|
.placeholder=${this.placeholder
|
||||||
@value-changed=${this._valueChanged}
|
? this.hass.areas[this.placeholder]?.name
|
||||||
|
: undefined}
|
||||||
|
.renderer=${this._rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._areaChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
const target = ev.target as HaComboBox;
|
||||||
const value = ev.detail.value;
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
if (!value) {
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
this._setValue(undefined);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = this._parseValue(value);
|
const filteredItems = fuzzyFilterSort<ScorableAreaFloorEntry>(
|
||||||
this._setValue(selected);
|
filterString,
|
||||||
|
target.items || []
|
||||||
|
);
|
||||||
|
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value?: AreaFloorValue) {
|
private get _value() {
|
||||||
this.value = value;
|
return this.value || "";
|
||||||
fireEvent(this, "value-changed", { value });
|
}
|
||||||
fireEvent(this, "change");
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _areaChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === "no_areas") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this.comboBox.selectedItem;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
id: selected.id,
|
||||||
|
type: selected.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { mdiPlus, mdiTextureBox } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { TemplateResult } from "lit";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query, 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";
|
||||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import type { ScorableTextItem } from "../common/string/filter/sequence-matching";
|
||||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
import { fuzzyFilterSort } from "../common/string/filter/sequence-matching";
|
||||||
|
import type { AreaRegistryEntry } from "../data/area_registry";
|
||||||
import { createAreaRegistryEntry } from "../data/area_registry";
|
import { createAreaRegistryEntry } from "../data/area_registry";
|
||||||
import type {
|
import type {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
@@ -20,15 +21,26 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
|||||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-generic-picker";
|
|
||||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
|
||||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>`}
|
||||||
|
${item.name}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
const ADD_NEW_ID = "___ADD_NEW___";
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
const NO_ITEMS_ID = "___NO_ITEMS___";
|
||||||
|
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||||
|
|
||||||
@customElement("ha-area-picker")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends LitElement {
|
export class HaAreaPicker extends LitElement {
|
||||||
@@ -87,68 +99,41 @@ export class HaAreaPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _suggestion?: string;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this.comboBox?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recompute value renderer when the areas change
|
public async focus() {
|
||||||
private _computeValueRenderer = memoizeOne(
|
await this.updateComplete;
|
||||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
await this.comboBox?.focus();
|
||||||
(value) => {
|
}
|
||||||
const area = this.hass.areas[value];
|
|
||||||
|
|
||||||
if (!area) {
|
|
||||||
return html`
|
|
||||||
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
|
|
||||||
<span slot="headline">${area}</span>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { floor } = getAreaContext(area, this.hass);
|
|
||||||
|
|
||||||
const areaName = area ? computeAreaName(area) : undefined;
|
|
||||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
|
||||||
|
|
||||||
const icon = area.icon;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${icon
|
|
||||||
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
slot="start"
|
|
||||||
.path=${mdiTextureBox}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
<span slot="headline">${areaName}</span>
|
|
||||||
${floorName
|
|
||||||
? html`<span slot="supporting-text">${floorName}</span>`
|
|
||||||
: nothing}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getAreas = memoizeOne(
|
private _getAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
haAreas: HomeAssistant["areas"],
|
areas: AreaRegistryEntry[],
|
||||||
haDevices: HomeAssistant["devices"],
|
devices: DeviceRegistryEntry[],
|
||||||
haEntities: HomeAssistant["entities"],
|
entities: EntityRegistryDisplayEntry[],
|
||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
|
noAdd: this["noAdd"],
|
||||||
excludeAreas: this["excludeAreas"]
|
excludeAreas: this["excludeAreas"]
|
||||||
): PickerComboBoxItem[] => {
|
): AreaRegistryEntry[] => {
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||||
|
|
||||||
const areas = Object.values(haAreas);
|
|
||||||
const devices = Object.values(haDevices);
|
|
||||||
const entities = Object.values(haEntities);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
includeDomains ||
|
includeDomains ||
|
||||||
excludeDomains ||
|
excludeDomains ||
|
||||||
@@ -278,147 +263,225 @@ export class HaAreaPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
if (!outputAreas.length) {
|
||||||
const { floor } = getAreaContext(area, this.hass);
|
outputAreas = [
|
||||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
{
|
||||||
const areaName = computeAreaName(area);
|
area_id: NO_ITEMS_ID,
|
||||||
return {
|
floor_id: null,
|
||||||
id: area.area_id,
|
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||||
primary: areaName || area.area_id,
|
picture: null,
|
||||||
secondary: floorName,
|
icon: null,
|
||||||
icon: area.icon || undefined,
|
aliases: [],
|
||||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
labels: [],
|
||||||
sorting_label: areaName,
|
temperature_entity_id: null,
|
||||||
search_labels: [
|
humidity_entity_id: null,
|
||||||
areaName,
|
created_at: 0,
|
||||||
floorName,
|
modified_at: 0,
|
||||||
area.area_id,
|
},
|
||||||
...area.aliases,
|
];
|
||||||
].filter((v): v is string => Boolean(v)),
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return items;
|
return noAdd
|
||||||
}
|
? outputAreas
|
||||||
);
|
: [
|
||||||
|
...outputAreas,
|
||||||
private _getItems = () =>
|
|
||||||
this._getAreas(
|
|
||||||
this.hass.areas,
|
|
||||||
this.hass.devices,
|
|
||||||
this.hass.entities,
|
|
||||||
this.includeDomains,
|
|
||||||
this.excludeDomains,
|
|
||||||
this.includeDeviceClasses,
|
|
||||||
this.deviceFilter,
|
|
||||||
this.entityFilter,
|
|
||||||
this.excludeAreas
|
|
||||||
);
|
|
||||||
|
|
||||||
private _allAreaNames = memoizeOne(
|
|
||||||
(areas: HomeAssistant["areas"]) =>
|
|
||||||
Object.values(areas)
|
|
||||||
.map((area) => computeAreaName(area)?.toLowerCase())
|
|
||||||
.filter(Boolean) as string[]
|
|
||||||
);
|
|
||||||
|
|
||||||
private _getAdditionalItems = (
|
|
||||||
searchString?: string
|
|
||||||
): PickerComboBoxItem[] => {
|
|
||||||
if (this.noAdd) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const allAreas = this._allAreaNames(this.hass.areas);
|
|
||||||
|
|
||||||
if (searchString && !allAreas.includes(searchString.toLowerCase())) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: ADD_NEW_ID + searchString,
|
|
||||||
primary: this.hass.localize(
|
|
||||||
"ui.components.area-picker.add_new_sugestion",
|
|
||||||
{
|
{
|
||||||
name: searchString,
|
area_id: ADD_NEW_ID,
|
||||||
}
|
floor_id: null,
|
||||||
),
|
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||||
icon_path: mdiPlus,
|
picture: null,
|
||||||
},
|
icon: "mdi:plus",
|
||||||
];
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
|
temperature_entity_id: null,
|
||||||
|
humidity_entity_id: null,
|
||||||
|
created_at: 0,
|
||||||
|
modified_at: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
protected updated(changedProps: PropertyValues) {
|
||||||
{
|
if (
|
||||||
id: ADD_NEW_ID,
|
(!this._init && this.hass) ||
|
||||||
primary: this.hass.localize("ui.components.area-picker.add_new"),
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
icon_path: mdiPlus,
|
) {
|
||||||
},
|
this._init = true;
|
||||||
];
|
const areas = this._getAreas(
|
||||||
};
|
Object.values(this.hass.areas),
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeAreas
|
||||||
|
).map((area) => ({
|
||||||
|
...area,
|
||||||
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
}));
|
||||||
|
this.comboBox.items = areas;
|
||||||
|
this.comboBox.filteredItems = areas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const placeholder =
|
|
||||||
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
|
||||||
|
|
||||||
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-combo-box
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.helper=${this.helper}
|
||||||
.label=${this.label}
|
item-value-path="area_id"
|
||||||
.notFoundLabel=${this.hass.localize(
|
item-id-path="area_id"
|
||||||
"ui.components.area-picker.no_match"
|
item-label-path="name"
|
||||||
)}
|
.value=${this._value}
|
||||||
.placeholder=${placeholder}
|
.disabled=${this.disabled}
|
||||||
.value=${this.value}
|
.required=${this.required}
|
||||||
.getItems=${this._getItems}
|
.label=${this.label === undefined && this.hass
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
? this.hass.localize("ui.components.area-picker.area")
|
||||||
.valueRenderer=${valueRenderer}
|
: this.label}
|
||||||
@value-changed=${this._valueChanged}
|
.placeholder=${this.placeholder
|
||||||
|
? this.hass.areas[this.placeholder]?.name
|
||||||
|
: undefined}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._areaChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
const target = ev.target as HaComboBox;
|
||||||
const value = ev.detail.value;
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
if (!value) {
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
this._setValue(undefined);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.startsWith(ADD_NEW_ID)) {
|
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||||
this.hass.loadFragmentTranslation("config");
|
filterString,
|
||||||
|
target.items?.filter(
|
||||||
|
(item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
if (filteredItems.length === 0) {
|
||||||
|
if (!this.noAdd) {
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
area_id: NO_ITEMS_ID,
|
||||||
|
floor_id: null,
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
picture: null,
|
||||||
|
labels: [],
|
||||||
|
aliases: [],
|
||||||
|
temperature_entity_id: null,
|
||||||
|
humidity_entity_id: null,
|
||||||
|
created_at: 0,
|
||||||
|
modified_at: 0,
|
||||||
|
},
|
||||||
|
] as AreaRegistryEntry[];
|
||||||
|
} else {
|
||||||
|
this._suggestion = filterString;
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
area_id: ADD_NEW_SUGGESTION_ID,
|
||||||
|
floor_id: null,
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.area-picker.add_new_sugestion",
|
||||||
|
{ name: this._suggestion }
|
||||||
|
),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
picture: null,
|
||||||
|
labels: [],
|
||||||
|
aliases: [],
|
||||||
|
temperature_entity_id: null,
|
||||||
|
humidity_entity_id: null,
|
||||||
|
created_at: 0,
|
||||||
|
modified_at: 0,
|
||||||
|
},
|
||||||
|
] as AreaRegistryEntry[];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const suggestedName = value.substring(ADD_NEW_ID.length);
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
showAreaRegistryDetailDialog(this, {
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
suggestedName: suggestedName,
|
this._opened = ev.detail.value;
|
||||||
createEntry: async (values) => {
|
}
|
||||||
try {
|
|
||||||
const area = await createAreaRegistryEntry(this.hass, values);
|
private _areaChanged(ev: ValueChangedEvent<string>) {
|
||||||
this._setValue(area.area_id);
|
ev.stopPropagation();
|
||||||
} catch (err: any) {
|
let newValue = ev.detail.value;
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
if (newValue === NO_ITEMS_ID) {
|
||||||
"ui.components.area-picker.failed_create_area"
|
newValue = "";
|
||||||
),
|
this.comboBox.setInputValue("");
|
||||||
text: err.message,
|
return;
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setValue(value);
|
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
|
showAreaRegistryDetailDialog(this, {
|
||||||
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
|
createEntry: async (values) => {
|
||||||
|
try {
|
||||||
|
const area = await createAreaRegistryEntry(this.hass, values);
|
||||||
|
const areas = [...Object.values(this.hass.areas), area];
|
||||||
|
this.comboBox.filteredItems = this._getAreas(
|
||||||
|
areas,
|
||||||
|
Object.values(this.hass.devices)!,
|
||||||
|
Object.values(this.hass.entities)!,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeAreas
|
||||||
|
);
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox.updateComplete;
|
||||||
|
this._setValue(area.area_id);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.components.area-picker.failed_create_area"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._suggestion = undefined;
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value?: string) {
|
private _setValue(value?: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
fireEvent(this, "value-changed", { value });
|
setTimeout(() => {
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,11 +5,8 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import {
|
import {
|
||||||
type PipelineRunEvent,
|
|
||||||
runAssistPipeline,
|
runAssistPipeline,
|
||||||
type AssistPipeline,
|
type AssistPipeline,
|
||||||
type ConversationChatLogAssistantDelta,
|
|
||||||
type ConversationChatLogToolResultDelta,
|
|
||||||
} from "../data/assist_pipeline";
|
} from "../data/assist_pipeline";
|
||||||
import { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
import { ConversationEntityFeature } from "../data/conversation";
|
import { ConversationEntityFeature } from "../data/conversation";
|
||||||
@@ -93,7 +90,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this._audioRecorder?.close();
|
this._audioRecorder?.close();
|
||||||
this._audioRecorder = undefined;
|
this._audioRecorder = undefined;
|
||||||
this._unloadAudio();
|
this._audio?.pause();
|
||||||
this._conversation = [];
|
this._conversation = [];
|
||||||
this._conversationId = null;
|
this._conversationId = null;
|
||||||
}
|
}
|
||||||
@@ -112,24 +109,25 @@ export class HaAssistChat extends LitElement {
|
|||||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="messages" id="scroll-container">
|
${controlHA
|
||||||
${controlHA
|
? nothing
|
||||||
? nothing
|
: html`
|
||||||
: html`
|
<ha-alert>
|
||||||
<ha-alert>
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.dialogs.voice_command.conversation_no_control"
|
||||||
"ui.dialogs.voice_command.conversation_no_control"
|
)}
|
||||||
)}
|
</ha-alert>
|
||||||
</ha-alert>
|
`}
|
||||||
`}
|
<div class="messages">
|
||||||
<div class="spacer"></div>
|
<div class="messages-container" id="scroll-container">
|
||||||
${this._conversation!.map(
|
${this._conversation!.map(
|
||||||
// New lines matter for messages
|
// New lines matter for messages
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
(message) => html`
|
(message) => html`
|
||||||
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input" slot="primaryAction">
|
<div class="input" slot="primaryAction">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
@@ -275,8 +273,8 @@ export class HaAssistChat extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _startListening() {
|
private async _startListening() {
|
||||||
this._unloadAudio();
|
|
||||||
this._processing = true;
|
this._processing = true;
|
||||||
|
this._audio?.pause();
|
||||||
if (!this._audioRecorder) {
|
if (!this._audioRecorder) {
|
||||||
this._audioRecorder = new AudioRecorder((audio) => {
|
this._audioRecorder = new AudioRecorder((audio) => {
|
||||||
if (this._audioBuffer) {
|
if (this._audioBuffer) {
|
||||||
@@ -295,36 +293,27 @@ export class HaAssistChat extends LitElement {
|
|||||||
await this._audioRecorder.start();
|
await this._audioRecorder.start();
|
||||||
|
|
||||||
this._addMessage(userMessage);
|
this._addMessage(userMessage);
|
||||||
|
this.requestUpdate("_audioRecorder");
|
||||||
|
|
||||||
const hassMessageProcesser = this._createAddHassMessageProcessor();
|
let continueConversation = false;
|
||||||
|
let hassMessage = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
let currentDeltaRole = "";
|
||||||
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
try {
|
try {
|
||||||
const unsub = await runAssistPipeline(
|
const unsub = await runAssistPipeline(
|
||||||
this.hass,
|
this.hass,
|
||||||
(event: PipelineRunEvent) => {
|
(event) => {
|
||||||
if (event.type === "run-start") {
|
if (event.type === "run-start") {
|
||||||
this._stt_binary_handler_id =
|
this._stt_binary_handler_id =
|
||||||
event.data.runner_data.stt_binary_handler_id;
|
event.data.runner_data.stt_binary_handler_id;
|
||||||
this._audio = new Audio(event.data.tts_output!.url);
|
|
||||||
this._audio.play();
|
|
||||||
this._audio.addEventListener("ended", () => {
|
|
||||||
this._unloadAudio();
|
|
||||||
if (hassMessageProcesser.continueConversation) {
|
|
||||||
this._startListening();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._audio.addEventListener("pause", this._unloadAudio);
|
|
||||||
this._audio.addEventListener("canplaythrough", () =>
|
|
||||||
this._audio?.play()
|
|
||||||
);
|
|
||||||
this._audio.addEventListener("error", () => {
|
|
||||||
this._unloadAudio();
|
|
||||||
showAlertDialog(this, { title: "Error playing audio." });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we start STT stage, the WS has a binary handler
|
// When we start STT stage, the WS has a binary handler
|
||||||
else if (event.type === "stt-start" && this._audioBuffer) {
|
if (event.type === "stt-start" && this._audioBuffer) {
|
||||||
// Send the buffer over the WS to the STT engine.
|
// Send the buffer over the WS to the STT engine.
|
||||||
for (const buffer of this._audioBuffer) {
|
for (const buffer of this._audioBuffer) {
|
||||||
this._sendAudioChunk(buffer);
|
this._sendAudioChunk(buffer);
|
||||||
@@ -333,26 +322,91 @@ export class HaAssistChat extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop recording if the server is done with STT stage
|
// Stop recording if the server is done with STT stage
|
||||||
else if (event.type === "stt-end") {
|
if (event.type === "stt-end") {
|
||||||
this._stt_binary_handler_id = undefined;
|
this._stt_binary_handler_id = undefined;
|
||||||
this._stopListening();
|
this._stopListening();
|
||||||
userMessage.text = event.data.stt_output.text;
|
userMessage.text = event.data.stt_output.text;
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
// Add the response message placeholder to the chat when we know the STT is done
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
hassMessageProcesser.addMessage();
|
this._addMessage(hassMessage);
|
||||||
} else if (event.type.startsWith("intent-")) {
|
}
|
||||||
hassMessageProcesser.processEvent(event);
|
|
||||||
} else if (event.type === "run-end") {
|
if (event.type === "intent-progress") {
|
||||||
|
const delta = event.data.chat_log_delta;
|
||||||
|
|
||||||
|
// new message
|
||||||
|
if (delta.role) {
|
||||||
|
// If currentDeltaRole exists, it means we're receiving our
|
||||||
|
// second or later message. Let's add it to the chat.
|
||||||
|
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
|
||||||
|
// Remove progress indicator of previous message
|
||||||
|
hassMessage.text = hassMessage.text.substring(
|
||||||
|
0,
|
||||||
|
hassMessage.text.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
hassMessage = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
this._addMessage(hassMessage);
|
||||||
|
}
|
||||||
|
currentDeltaRole = delta.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentDeltaRole === "assistant" &&
|
||||||
|
"content" in delta &&
|
||||||
|
delta.content
|
||||||
|
) {
|
||||||
|
hassMessage.text =
|
||||||
|
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||||
|
delta.content +
|
||||||
|
"…";
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "intent-end") {
|
||||||
|
this._conversationId = event.data.intent_output.conversation_id;
|
||||||
|
continueConversation =
|
||||||
|
event.data.intent_output.continue_conversation;
|
||||||
|
const plain = event.data.intent_output.response.speech?.plain;
|
||||||
|
if (plain) {
|
||||||
|
hassMessage.text = plain.speech;
|
||||||
|
}
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "tts-end") {
|
||||||
|
const url = event.data.tts_output.url;
|
||||||
|
this._audio = new Audio(url);
|
||||||
|
this._audio.play();
|
||||||
|
this._audio.addEventListener("ended", () => {
|
||||||
|
this._unloadAudio();
|
||||||
|
if (continueConversation) {
|
||||||
|
this._startListening();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._audio.addEventListener("pause", this._unloadAudio);
|
||||||
|
this._audio.addEventListener("canplaythrough", this._playAudio);
|
||||||
|
this._audio.addEventListener("error", this._audioError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "run-end") {
|
||||||
this._stt_binary_handler_id = undefined;
|
this._stt_binary_handler_id = undefined;
|
||||||
unsub();
|
unsub();
|
||||||
} else if (event.type === "error") {
|
}
|
||||||
this._unloadAudio();
|
|
||||||
|
if (event.type === "error") {
|
||||||
this._stt_binary_handler_id = undefined;
|
this._stt_binary_handler_id = undefined;
|
||||||
if (userMessage.text === "…") {
|
if (userMessage.text === "…") {
|
||||||
userMessage.text = event.data.message;
|
userMessage.text = event.data.message;
|
||||||
userMessage.error = true;
|
userMessage.error = true;
|
||||||
} else {
|
} else {
|
||||||
hassMessageProcesser.setError(event.data.message);
|
hassMessage.text = event.data.message;
|
||||||
|
hassMessage.error = true;
|
||||||
}
|
}
|
||||||
this._stopListening();
|
this._stopListening();
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
@@ -410,33 +464,90 @@ export class HaAssistChat extends LitElement {
|
|||||||
this.hass.connection.socket!.send(data);
|
this.hass.connection.socket!.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _playAudio = () => {
|
||||||
|
this._audio?.play();
|
||||||
|
};
|
||||||
|
|
||||||
|
private _audioError = () => {
|
||||||
|
showAlertDialog(this, { title: "Error playing audio." });
|
||||||
|
this._audio?.removeAttribute("src");
|
||||||
|
};
|
||||||
|
|
||||||
private _unloadAudio = () => {
|
private _unloadAudio = () => {
|
||||||
if (!this._audio) {
|
this._audio?.removeAttribute("src");
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._audio.pause();
|
|
||||||
this._audio.removeAttribute("src");
|
|
||||||
this._audio = undefined;
|
this._audio = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private async _processText(text: string) {
|
private async _processText(text: string) {
|
||||||
this._unloadAudio();
|
|
||||||
this._processing = true;
|
this._processing = true;
|
||||||
|
this._audio?.pause();
|
||||||
this._addMessage({ who: "user", text });
|
this._addMessage({ who: "user", text });
|
||||||
const hassMessageProcesser = this._createAddHassMessageProcessor();
|
let hassMessage = {
|
||||||
hassMessageProcesser.addMessage();
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
let currentDeltaRole = "";
|
||||||
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
|
this._addMessage(hassMessage);
|
||||||
try {
|
try {
|
||||||
const unsub = await runAssistPipeline(
|
const unsub = await runAssistPipeline(
|
||||||
this.hass,
|
this.hass,
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.type.startsWith("intent-")) {
|
if (event.type === "intent-progress") {
|
||||||
hassMessageProcesser.processEvent(event);
|
const delta = event.data.chat_log_delta;
|
||||||
|
|
||||||
|
// new message and previous message has content
|
||||||
|
if (delta.role) {
|
||||||
|
// If currentDeltaRole exists, it means we're receiving our
|
||||||
|
// second or later message. Let's add it to the chat.
|
||||||
|
if (
|
||||||
|
currentDeltaRole &&
|
||||||
|
delta.role === "assistant" &&
|
||||||
|
hassMessage.text !== "…"
|
||||||
|
) {
|
||||||
|
// Remove progress indicator of previous message
|
||||||
|
hassMessage.text = hassMessage.text.substring(
|
||||||
|
0,
|
||||||
|
hassMessage.text.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
hassMessage = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
this._addMessage(hassMessage);
|
||||||
|
}
|
||||||
|
currentDeltaRole = delta.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentDeltaRole === "assistant" &&
|
||||||
|
"content" in delta &&
|
||||||
|
delta.content
|
||||||
|
) {
|
||||||
|
hassMessage.text =
|
||||||
|
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||||
|
delta.content +
|
||||||
|
"…";
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "intent-end") {
|
if (event.type === "intent-end") {
|
||||||
|
this._conversationId = event.data.intent_output.conversation_id;
|
||||||
|
const plain = event.data.intent_output.response.speech?.plain;
|
||||||
|
if (plain) {
|
||||||
|
hassMessage.text = plain.speech;
|
||||||
|
}
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
unsub();
|
unsub();
|
||||||
}
|
}
|
||||||
if (event.type === "error") {
|
if (event.type === "error") {
|
||||||
hassMessageProcesser.setError(event.data.message);
|
hassMessage.text = event.data.message;
|
||||||
|
hassMessage.error = true;
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
unsub();
|
unsub();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -449,126 +560,20 @@ export class HaAssistChat extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
hassMessageProcesser.setError(
|
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||||
this.hass.localize("ui.dialogs.voice_command.error")
|
hassMessage.error = true;
|
||||||
);
|
this.requestUpdate("_conversation");
|
||||||
} finally {
|
} finally {
|
||||||
this._processing = false;
|
this._processing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createAddHassMessageProcessor() {
|
|
||||||
let currentDeltaRole = "";
|
|
||||||
|
|
||||||
const progressToNextMessage = () => {
|
|
||||||
if (progress.hassMessage.text === "…") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
progress.hassMessage.text = progress.hassMessage.text.substring(
|
|
||||||
0,
|
|
||||||
progress.hassMessage.text.length - 1
|
|
||||||
);
|
|
||||||
|
|
||||||
progress.hassMessage = {
|
|
||||||
who: "hass",
|
|
||||||
text: "…",
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
this._addMessage(progress.hassMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAssistantDelta = (
|
|
||||||
_delta: any
|
|
||||||
): _delta is Partial<ConversationChatLogAssistantDelta> =>
|
|
||||||
currentDeltaRole === "assistant";
|
|
||||||
|
|
||||||
const isToolResult = (
|
|
||||||
_delta: any
|
|
||||||
): _delta is ConversationChatLogToolResultDelta =>
|
|
||||||
currentDeltaRole === "tool_result";
|
|
||||||
|
|
||||||
const tools: Record<
|
|
||||||
string,
|
|
||||||
ConversationChatLogAssistantDelta["tool_calls"][0]
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
const progress = {
|
|
||||||
continueConversation: false,
|
|
||||||
hassMessage: {
|
|
||||||
who: "hass",
|
|
||||||
text: "…",
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
addMessage: () => {
|
|
||||||
this._addMessage(progress.hassMessage);
|
|
||||||
},
|
|
||||||
setError: (error: string) => {
|
|
||||||
progressToNextMessage();
|
|
||||||
progress.hassMessage.text = error;
|
|
||||||
progress.hassMessage.error = true;
|
|
||||||
this.requestUpdate("_conversation");
|
|
||||||
},
|
|
||||||
processEvent: (event: PipelineRunEvent) => {
|
|
||||||
if (event.type === "intent-progress") {
|
|
||||||
const delta = event.data.chat_log_delta;
|
|
||||||
|
|
||||||
// new message
|
|
||||||
if (delta.role) {
|
|
||||||
progressToNextMessage();
|
|
||||||
currentDeltaRole = delta.role;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAssistantDelta(delta)) {
|
|
||||||
if (delta.content) {
|
|
||||||
progress.hassMessage.text =
|
|
||||||
progress.hassMessage.text.substring(
|
|
||||||
0,
|
|
||||||
progress.hassMessage.text.length - 1
|
|
||||||
) +
|
|
||||||
delta.content +
|
|
||||||
"…";
|
|
||||||
this.requestUpdate("_conversation");
|
|
||||||
}
|
|
||||||
if (delta.tool_calls) {
|
|
||||||
for (const toolCall of delta.tool_calls) {
|
|
||||||
tools[toolCall.id] = toolCall;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isToolResult(delta)) {
|
|
||||||
if (tools[delta.tool_call_id]) {
|
|
||||||
delete tools[delta.tool_call_id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (event.type === "intent-end") {
|
|
||||||
this._conversationId = event.data.intent_output.conversation_id;
|
|
||||||
progress.continueConversation =
|
|
||||||
event.data.intent_output.continue_conversation;
|
|
||||||
const response =
|
|
||||||
event.data.intent_output.response.speech?.plain.speech;
|
|
||||||
if (!response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.data.intent_output.response.response_type === "error") {
|
|
||||||
progress.setError(response);
|
|
||||||
} else {
|
|
||||||
progress.hassMessage.text = response;
|
|
||||||
this.requestUpdate("_conversation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
ha-alert {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -576,30 +581,30 @@ export class HaAssistChat extends LitElement {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.messages-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 0px 10px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 12px 16px;
|
|
||||||
}
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
.message {
|
.message {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 18px;
|
||||||
clear: both;
|
clear: both;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
.message:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
.message {
|
.message {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +619,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-inline-start: 24px;
|
margin-inline-start: 24px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
align-self: flex-end;
|
float: var(--float-end);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||||
@@ -626,7 +631,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
margin-inline-end: 24px;
|
margin-inline-end: 24px;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
align-self: flex-start;
|
float: var(--float-start);
|
||||||
border-bottom-left-radius: 0px;
|
border-bottom-left-radius: 0px;
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--chat-background-color-hass,
|
--chat-background-color-hass,
|
||||||
|
@@ -95,9 +95,9 @@ export class HaBadge extends LitElement {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.label {
|
.label {
|
||||||
font-size: var(--ha-font-size-xs);
|
font-size: 10px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
@@ -105,8 +105,8 @@ export class HaBadge extends LitElement {
|
|||||||
.content {
|
.content {
|
||||||
font-size: var(--ha-badge-font-size, 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: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 16px;
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
@@ -81,27 +81,27 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
/**
|
/**
|
||||||
* Label for the day input
|
* Label for the day input
|
||||||
*/
|
*/
|
||||||
@property({ type: String, attribute: "day-label" }) dayLabel = "";
|
@property({ attribute: false }) dayLabel = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the hour input
|
* Label for the hour input
|
||||||
*/
|
*/
|
||||||
@property({ type: String, attribute: "hour-label" }) hourLabel = "";
|
@property({ attribute: false }) hourLabel = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the min input
|
* Label for the min input
|
||||||
*/
|
*/
|
||||||
@property({ type: String, attribute: "min-label" }) minLabel = "";
|
@property({ attribute: false }) minLabel = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the sec input
|
* Label for the sec input
|
||||||
*/
|
*/
|
||||||
@property({ type: String, attribute: "sec-label" }) secLabel = "";
|
@property({ attribute: false }) secLabel = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for the milli sec input
|
* Label for the milli sec input
|
||||||
*/
|
*/
|
||||||
@property({ type: String, attribute: "ms-label" }) millisecLabel = "";
|
@property({ attribute: false }) millisecLabel = "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* show the sec field
|
* show the sec field
|
||||||
@@ -342,7 +342,7 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
padding-right: 3px;
|
padding-right: 3px;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
width: 60px;
|
width: 55px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
--mdc-shape-small: 0;
|
--mdc-shape-small: 0;
|
||||||
@@ -381,21 +381,15 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
-webkit-font-smoothing: antialiased;
|
||||||
font-family: var(
|
font-family: var(
|
||||||
--mdc-typography-body2-font-family,
|
--mdc-typography-body2-font-family,
|
||||||
var(--mdc-typography-font-family, var(--ha-font-family-body))
|
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||||
);
|
|
||||||
font-size: var(--mdc-typography-body2-font-size, var(--ha-font-size-s));
|
|
||||||
line-height: var(
|
|
||||||
--mdc-typography-body2-line-height,
|
|
||||||
var(--ha-line-height-condensed)
|
|
||||||
);
|
|
||||||
font-weight: var(
|
|
||||||
--mdc-typography-body2-font-weight,
|
|
||||||
var(--ha-font-weight-normal)
|
|
||||||
);
|
);
|
||||||
|
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
|
||||||
|
line-height: var(--mdc-typography-body2-line-height, 1.25rem);
|
||||||
|
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||||
letter-spacing: var(
|
letter-spacing: var(
|
||||||
--mdc-typography-body2-letter-spacing,
|
--mdc-typography-body2-letter-spacing,
|
||||||
0.0178571429em
|
0.0178571429em
|
||||||
@@ -409,7 +403,7 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-input-helper-text {
|
ha-input-helper-text {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: normal;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -92,7 +92,7 @@ export class HaBigNumber extends LitElement {
|
|||||||
}
|
}
|
||||||
.value .unit {
|
.value .unit {
|
||||||
font-size: 0.33em;
|
font-size: 0.33em;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 1.26;
|
||||||
}
|
}
|
||||||
/* Accessibility */
|
/* Accessibility */
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
|
@@ -41,14 +41,14 @@ export class HaCard extends LitElement {
|
|||||||
:host ::slotted(.card-header) {
|
:host ::slotted(.card-header) {
|
||||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||||
font-family: var(--ha-card-header-font-family, inherit);
|
font-family: var(--ha-card-header-font-family, inherit);
|
||||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
font-size: var(--ha-card-header-font-size, 24px);
|
||||||
letter-spacing: -0.012em;
|
letter-spacing: -0.012em;
|
||||||
line-height: var(--ha-line-height-expanded);
|
line-height: 48px;
|
||||||
padding: 12px 16px 16px;
|
padding: 12px 16px 16px;
|
||||||
display: block;
|
display: block;
|
||||||
margin-block-start: 0px;
|
margin-block-start: 0px;
|
||||||
margin-block-end: 0px;
|
margin-block-end: 0px;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::slotted(.card-content:not(:first-child)),
|
:host ::slotted(.card-content:not(:first-child)),
|
||||||
|
@@ -154,7 +154,7 @@ class HaClimateState extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.state-label {
|
.state-label {
|
||||||
font-weight: var(--ha-font-weight-bold);
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unit {
|
.unit {
|
||||||
|
@@ -21,13 +21,13 @@ export class HaComboBoxItem extends HaMdListItem {
|
|||||||
--state-icon-color: var(--secondary-text-color);
|
--state-icon-color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
[slot="headline"] {
|
[slot="headline"] {
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 22px;
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
[slot="supporting-text"] {
|
[slot="supporting-text"] {
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 18px;
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
::slotted(state-badge),
|
::slotted(state-badge),
|
||||||
@@ -35,20 +35,6 @@ 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);
|
|
||||||
}
|
|
||||||
::slotted(.domain) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { HaTextField } from "./ha-textfield";
|
|
||||||
|
|
||||||
@customElement("ha-combo-box-textfield")
|
|
||||||
export class HaComboBoxTextField extends HaTextField {
|
|
||||||
@property({ type: Boolean, attribute: "disable-set-value" })
|
|
||||||
public disableSetValue = false;
|
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
|
||||||
super.willUpdate(changedProps);
|
|
||||||
if (changedProps.has("value")) {
|
|
||||||
if (this.disableSetValue) {
|
|
||||||
this.value = changedProps.get("value") as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-combo-box-textfield": HaComboBoxTextField;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -12,12 +12,11 @@ import type {
|
|||||||
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
import "./ha-combo-box-textfield";
|
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import type { HaTextField } from "./ha-textfield";
|
import type { HaTextField } from "./ha-textfield";
|
||||||
@@ -109,14 +108,9 @@ export class HaComboBox extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||||
public hideClearIcon = false;
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "clear-initial-value" })
|
|
||||||
public clearInitialValue = false;
|
|
||||||
|
|
||||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||||
|
|
||||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
@query("ha-textfield", true) private _inputElement!: HaTextField;
|
||||||
|
|
||||||
@state({ type: Boolean }) private _disableSetValue = false;
|
|
||||||
|
|
||||||
private _overlayMutationObserver?: MutationObserver;
|
private _overlayMutationObserver?: MutationObserver;
|
||||||
|
|
||||||
@@ -177,7 +171,7 @@ export class HaComboBox extends LitElement {
|
|||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
attr-for-value="value"
|
attr-for-value="value"
|
||||||
>
|
>
|
||||||
<ha-combo-box-textfield
|
<ha-textfield
|
||||||
label=${ifDefined(this.label)}
|
label=${ifDefined(this.label)}
|
||||||
placeholder=${ifDefined(this.placeholder)}
|
placeholder=${ifDefined(this.placeholder)}
|
||||||
?disabled=${this.disabled}
|
?disabled=${this.disabled}
|
||||||
@@ -197,10 +191,9 @@ export class HaComboBox extends LitElement {
|
|||||||
.invalid=${this.invalid}
|
.invalid=${this.invalid}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
helperPersistent
|
helperPersistent
|
||||||
.disableSetValue=${this._disableSetValue}
|
|
||||||
>
|
>
|
||||||
<slot name="icon" slot="leadingIcon"></slot>
|
<slot name="icon" slot="leadingIcon"></slot>
|
||||||
</ha-combo-box-textfield>
|
</ha-textfield>
|
||||||
${this.value && !this.hideClearIcon
|
${this.value && !this.hideClearIcon
|
||||||
? html`<ha-svg-icon
|
? html`<ha-svg-icon
|
||||||
role="button"
|
role="button"
|
||||||
@@ -253,20 +246,8 @@ export class HaComboBox extends LitElement {
|
|||||||
// delay this so we can handle click event for toggle button before setting _opened
|
// delay this so we can handle click event for toggle button before setting _opened
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.opened = opened;
|
this.opened = opened;
|
||||||
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
|
||||||
}, 0);
|
}, 0);
|
||||||
|
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
||||||
if (this.clearInitialValue) {
|
|
||||||
this.setTextFieldValue("");
|
|
||||||
if (opened) {
|
|
||||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
|
||||||
setTimeout(() => {
|
|
||||||
this._disableSetValue = false;
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
this._disableSetValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opened) {
|
if (opened) {
|
||||||
const overlay = document.querySelector<HTMLElement>(
|
const overlay = document.querySelector<HTMLElement>(
|
||||||
@@ -361,10 +342,10 @@ export class HaComboBox extends LitElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
--vaadin-combo-box-overlay-max-height: calc(45vh - 56px);
|
||||||
}
|
}
|
||||||
ha-combo-box-textfield {
|
ha-textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
ha-combo-box-textfield > ha-icon-button {
|
ha-textfield > ha-icon-button {
|
||||||
--mdc-icon-button-size: 24px;
|
--mdc-icon-button-size: 24px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
@@ -58,8 +58,8 @@ export class HaControlButton extends LitElement {
|
|||||||
padding: var(--control-button-padding);
|
padding: var(--control-button-padding);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
font-family: var(--ha-font-family-body);
|
font-family: Roboto;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: none;
|
background: none;
|
||||||
|
@@ -194,7 +194,7 @@ export class HaControlNumberButton extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
:host([disabled]) {
|
:host([disabled]) {
|
||||||
|
@@ -179,7 +179,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
--control-select-menu-padding: 6px 10px;
|
--control-select-menu-padding: 6px 10px;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
--ha-ripple-color: var(--secondary-text-color);
|
--ha-ripple-color: var(--secondary-text-color);
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
width: auto;
|
width: auto;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
@@ -208,7 +208,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
letter-spacing: 0.25px;
|
letter-spacing: 0.25px;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
|
@@ -207,7 +207,7 @@ export class HaControlSelect extends LitElement {
|
|||||||
outline: none;
|
outline: none;
|
||||||
transition: box-shadow 180ms ease-in-out;
|
transition: box-shadow 180ms ease-in-out;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
@@ -368,7 +368,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
--control-slider-thickness: 40px;
|
--control-slider-thickness: 40px;
|
||||||
--control-slider-border-radius: 10px;
|
--control-slider-border-radius: 10px;
|
||||||
--control-slider-tooltip-font-size: var(--ha-font-size-m);
|
--control-slider-tooltip-font-size: 14px;
|
||||||
height: var(--control-slider-thickness);
|
height: var(--control-slider-thickness);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--control-slider-border-radius);
|
border-radius: var(--control-slider-border-radius);
|
||||||
|
@@ -53,12 +53,12 @@ export class HaDialogHeader extends LitElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.header-title {
|
.header-title {
|
||||||
font-size: var(--ha-font-size-xl);
|
font-size: 22px;
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: 28px;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
@@ -85,12 +85,12 @@ export class HaDialog extends DialogBase {
|
|||||||
var(--dialog-backdrop-filter, none)
|
var(--dialog-backdrop-filter, none)
|
||||||
);
|
);
|
||||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
--mdc-typography-headline6-font-weight: 400;
|
||||||
--mdc-typography-headline6-font-size: 1.574rem;
|
--mdc-typography-headline6-font-size: 1.574rem;
|
||||||
}
|
}
|
||||||
.mdc-dialog__actions {
|
.mdc-dialog__actions {
|
||||||
justify-content: var(--justify-action-buttons, flex-end);
|
justify-content: var(--justify-action-buttons, flex-end);
|
||||||
padding: 12px 24px max(var(--safe-area-inset-bottom), 12px) 24px;
|
padding-bottom: max(env(safe-area-inset-bottom), 24px);
|
||||||
}
|
}
|
||||||
.mdc-dialog__actions span:nth-child(1) {
|
.mdc-dialog__actions span:nth-child(1) {
|
||||||
flex: var(--secondary-action-button-flex, unset);
|
flex: var(--secondary-action-button-flex, unset);
|
||||||
@@ -107,6 +107,9 @@ export class HaDialog extends DialogBase {
|
|||||||
.mdc-dialog__title:has(span) {
|
.mdc-dialog__title:has(span) {
|
||||||
padding: 12px 12px 0;
|
padding: 12px 12px 0;
|
||||||
}
|
}
|
||||||
|
.mdc-dialog__actions {
|
||||||
|
padding: 12px 24px 12px 24px;
|
||||||
|
}
|
||||||
.mdc-dialog__title::before {
|
.mdc-dialog__title::before {
|
||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
@@ -117,7 +120,7 @@ export class HaDialog extends DialogBase {
|
|||||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||||
padding-bottom: max(
|
padding-bottom: max(
|
||||||
var(--dialog-content-padding, 24px),
|
var(--dialog-content-padding, 24px),
|
||||||
var(--safe-area-inset-bottom)
|
env(safe-area-inset-bottom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.mdc-dialog .mdc-dialog__surface {
|
.mdc-dialog .mdc-dialog__surface {
|
||||||
|
@@ -52,11 +52,11 @@ class HaDurationInput extends LitElement {
|
|||||||
.milliseconds=${this._milliseconds}
|
.milliseconds=${this._milliseconds}
|
||||||
@value-changed=${this._durationChanged}
|
@value-changed=${this._durationChanged}
|
||||||
no-hours-limit
|
no-hours-limit
|
||||||
day-label="dd"
|
dayLabel="dd"
|
||||||
hour-label="hh"
|
hourLabel="hh"
|
||||||
min-label="mm"
|
minLabel="mm"
|
||||||
sec-label="ss"
|
secLabel="ss"
|
||||||
ms-label="ms"
|
millisecLabel="ms"
|
||||||
></ha-base-time-input>
|
></ha-base-time-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -188,7 +188,7 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#summary.noCollapse {
|
#summary.noCollapse {
|
||||||
@@ -202,7 +202,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
.header,
|
.header,
|
||||||
::slotted([slot="header"]) {
|
::slotted([slot="header"]) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -219,7 +218,7 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
.secondary {
|
.secondary {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -294,7 +294,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
}
|
}
|
||||||
.supports {
|
.supports {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
font-size: var(--ha-font-size-s);
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
:host([disabled]) .secondary {
|
:host([disabled]) .secondary {
|
||||||
color: var(--disabled-text-color);
|
color: var(--disabled-text-color);
|
||||||
@@ -324,7 +324,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.progress {
|
.progress {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
@@ -333,7 +333,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: 14px;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@@ -208,10 +208,10 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: var(--ha-font-size-xs);
|
font-weight: 400;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-size: 11px;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
@@ -303,10 +303,10 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: var(--ha-font-size-xs);
|
font-weight: 400;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-size: 11px;
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user