mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-16 20:59:26 +00:00
Compare commits
39 Commits
add-Use-UU
...
switch_as_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
63a316cc68 | ||
![]() |
90b7abc370 | ||
![]() |
f3feb24435 | ||
![]() |
366d55306d | ||
![]() |
52c918523f | ||
![]() |
f25cfb73ab | ||
![]() |
13a7902fd4 | ||
![]() |
bad776b979 | ||
![]() |
396791b805 | ||
![]() |
2b1457e1cd | ||
![]() |
b5861869e3 | ||
![]() |
9444228907 | ||
![]() |
86afd883a5 | ||
![]() |
062f21aa91 | ||
![]() |
ba235ac797 | ||
![]() |
505c22248b | ||
![]() |
624cb48f78 | ||
![]() |
7ab54ee5ce | ||
![]() |
f5af63a50e | ||
![]() |
ff80ab34ee | ||
![]() |
cfc1999a28 | ||
![]() |
7ca28469b7 | ||
![]() |
ac670614b4 | ||
![]() |
e263b57296 | ||
![]() |
c7050e4676 | ||
![]() |
00cbd1d9e6 | ||
![]() |
2a12172eeb | ||
![]() |
85d3011625 | ||
![]() |
ca22ec6340 | ||
![]() |
61f6e8855b | ||
![]() |
a44b8981e1 | ||
![]() |
b080bca9ce | ||
![]() |
d30e8ee9d8 | ||
![]() |
637e4203e5 | ||
![]() |
2648a53bbc | ||
![]() |
b3fa0cccb4 | ||
![]() |
dd963be723 | ||
![]() |
224df896a1 | ||
![]() |
a58b4fb262 |
@@ -53,13 +53,19 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmit(ev) {
|
||||
|
11
gallery/src/data/text.ts
Normal file
11
gallery/src/data/text.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const LONG_TEXT = `
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
|
||||
|
||||
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
|
||||
|
||||
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
|
||||
|
||||
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
|
||||
|
||||
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
|
||||
`;
|
@@ -3,18 +3,7 @@ import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-faded";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
|
||||
const LONG_TEXT = `
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
|
||||
|
||||
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
|
||||
|
||||
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
|
||||
|
||||
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
|
||||
|
||||
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
|
||||
`;
|
||||
import { LONG_TEXT } from "../../data/text";
|
||||
|
||||
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
|
||||
|
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import "../../components/demo-black-white-row";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -147,7 +147,9 @@ const SCHEMAS: {
|
||||
{ name: "target", selector: { target: {} } },
|
||||
{ name: "number", selector: { number: { min: 0, max: 10 } } },
|
||||
{ name: "boolean", selector: { boolean: {} } },
|
||||
{ name: "time", selector: { time: {} } },
|
||||
{ name: "time", required: true, selector: { time: {} } },
|
||||
{ name: "datetime", required: true, selector: { datetime: {} } },
|
||||
{ name: "date", required: true, selector: { date: {} } },
|
||||
{ name: "action", selector: { action: {} } },
|
||||
{ name: "text", selector: { text: { multiline: false } } },
|
||||
{ name: "text_multiline", selector: { text: { multiline: true } } },
|
||||
|
@@ -1,20 +1,20 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import "@material/mwc-button";
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
|
||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -109,7 +109,7 @@ const AREAS = [
|
||||
|
||||
const SCHEMAS: {
|
||||
name: string;
|
||||
input: Record<string, BlueprintInput | null>;
|
||||
input: Record<string, (BlueprintInput & { required?: boolean }) | null>;
|
||||
}[] = [
|
||||
{
|
||||
name: "One of each",
|
||||
@@ -166,7 +166,9 @@ const SCHEMAS: {
|
||||
object: { name: "Object", selector: { object: {} } },
|
||||
select_radio: {
|
||||
name: "Select (Radio)",
|
||||
selector: { select: { options: ["Option 1", "Option 2"] } },
|
||||
selector: {
|
||||
select: { options: ["Option 1", "Option 2"], mode: "list" },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
name: "Select",
|
||||
@@ -183,6 +185,22 @@ const SCHEMAS: {
|
||||
},
|
||||
},
|
||||
},
|
||||
select_custom: {
|
||||
name: "Select (Custom)",
|
||||
selector: {
|
||||
select: {
|
||||
custom_value: true,
|
||||
options: [
|
||||
"Option 1",
|
||||
"Option 2",
|
||||
"Option 3",
|
||||
"Option 4",
|
||||
"Option 5",
|
||||
"Option 6",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
location: { name: "Location", selector: { location: {} } },
|
||||
@@ -202,6 +220,35 @@ const SCHEMAS: {
|
||||
input: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||
select: {
|
||||
name: "Select Multiple",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: [
|
||||
"Option 1",
|
||||
"Option 2",
|
||||
"Option 3",
|
||||
"Option 4",
|
||||
"Option 5",
|
||||
"Option 6",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
select_checkbox: {
|
||||
name: "Select Multiple (Checkbox)",
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: ["Option 1", "Option 2", "Option 3", "Option 4"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -210,6 +257,12 @@ const SCHEMAS: {
|
||||
class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
@state() public hass!: HomeAssistant;
|
||||
|
||||
@state() private _disabled = false;
|
||||
|
||||
@state() private _required = false;
|
||||
|
||||
@state() private _label = true;
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
constructor() {
|
||||
@@ -343,6 +396,29 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="options">
|
||||
<ha-formfield label="Labels">
|
||||
<ha-switch
|
||||
.name=${"label"}
|
||||
.checked=${this._label}
|
||||
@change=${this._handleOptionChange}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Required">
|
||||
<ha-switch
|
||||
.name=${"required"}
|
||||
.checked=${this._required}
|
||||
@change=${this._handleOptionChange}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Disabled">
|
||||
<ha-switch
|
||||
.name=${"disabled"}
|
||||
.checked=${this._disabled}
|
||||
@change=${this._handleOptionChange}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${SCHEMAS.map((info, idx) => {
|
||||
const data = this.data[idx];
|
||||
const valueChanged = (ev) => {
|
||||
@@ -365,7 +441,10 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
.key=${key}
|
||||
.label=${this._label ? value!.name : undefined}
|
||||
.value=${data[key] ?? value!.default}
|
||||
.disabled=${this._disabled}
|
||||
.required=${this._required}
|
||||
@value-changed=${valueChanged}
|
||||
></ha-selector>
|
||||
</ha-settings-row>
|
||||
@@ -378,10 +457,20 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleOptionChange(ev) {
|
||||
this[`_${ev.target.name}`] = ev.target.checked;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-selector {
|
||||
width: 60;
|
||||
}
|
||||
.options {
|
||||
padding: 16px 48px;
|
||||
}
|
||||
.options ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ const CONFIGS = [
|
||||
heading: "markdown-it demo",
|
||||
config: `
|
||||
- type: markdown
|
||||
content: >
|
||||
content: >-
|
||||
# h1 Heading 8-)
|
||||
|
||||
## h2 Heading
|
||||
@@ -249,6 +249,17 @@ const CONFIGS = [
|
||||
::: warning
|
||||
*here be dragons*
|
||||
:::
|
||||
|
||||
### ha-alert
|
||||
|
||||
You can use our [\`ha-alert\`](https://design.home-assistant.io/#components/ha-alert) component in markdown content rendered in the Home Assistant Frontend.
|
||||
|
||||
<ha-alert alert-type="error">This is an error alert — check it out!</ha-alert>
|
||||
<ha-alert alert-type="warning">This is a warning alert — check it out!</ha-alert>
|
||||
<ha-alert alert-type="info">This is an info alert — check it out!</ha-alert>
|
||||
<ha-alert alert-type="success">This is a success alert — check it out!</ha-alert>
|
||||
<ha-alert title="Test alert">This is an alert with a title</ha-alert>
|
||||
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
UPDATE_SUPPORT_RELEASE_NOTES,
|
||||
} from "../../../../src/data/update";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { LONG_TEXT } from "../../data/text";
|
||||
|
||||
const base_attributes = {
|
||||
title: "Awesome",
|
||||
@@ -108,6 +110,24 @@ const ENTITIES = [
|
||||
latest_version: null,
|
||||
friendly_name: "Update without latest_version",
|
||||
}),
|
||||
getEntity("update", "update16", "off", {
|
||||
...base_attributes,
|
||||
friendly_name: "Update with release notes",
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
|
||||
}),
|
||||
getEntity("update", "update17", "off", {
|
||||
...base_attributes,
|
||||
friendly_name: "Update with release notes error",
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
|
||||
}),
|
||||
getEntity("update", "update18", "off", {
|
||||
...base_attributes,
|
||||
friendly_name: "Update with release notes loading",
|
||||
supported_features:
|
||||
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-update")
|
||||
@@ -130,6 +150,24 @@ class DemoMoreInfoUpdate extends LitElement {
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
hass.mockWS(
|
||||
"update/release_notes",
|
||||
(msg: { type: string; entity_id: string }) => {
|
||||
if (msg.entity_id === "update.update16") {
|
||||
return LONG_TEXT;
|
||||
}
|
||||
if (msg.entity_id === "update.update17") {
|
||||
return Promise.reject({
|
||||
code: "error",
|
||||
message: "Could not fetch release notes",
|
||||
});
|
||||
}
|
||||
if (msg.entity_id === "update.update18") {
|
||||
return undefined;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = home-assistant-frontend
|
||||
version = 20220322.0
|
||||
version = 20220329.0
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
@@ -7,6 +7,7 @@ export const iconColorCSS = css`
|
||||
ha-state-icon[data-domain="calendar"][data-state="on"],
|
||||
ha-state-icon[data-domain="camera"][data-state="streaming"],
|
||||
ha-state-icon[data-domain="cover"][data-state="open"],
|
||||
ha-state-icon[data-domain="device_tracker"][data-state="home"],
|
||||
ha-state-icon[data-domain="fan"][data-state="on"],
|
||||
ha-state-icon[data-domain="humidifier"][data-state="on"],
|
||||
ha-state-icon[data-domain="light"][data-state="on"],
|
||||
|
53
src/common/util/time-cache-entity-promise-func.ts
Normal file
53
src/common/util/time-cache-entity-promise-func.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
interface ResultCache<T> {
|
||||
[entityId: string]: Promise<T> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a function with result caching per entity.
|
||||
* @param cacheKey key to store the cache on hass object
|
||||
* @param cacheTime time to cache the results
|
||||
* @param func function to fetch the data
|
||||
* @param hass Home Assistant object
|
||||
* @param entityId entity to fetch data for
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
*/
|
||||
export const timeCacheEntityPromiseFunc = async <T>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
|
||||
|
||||
if (!cache) {
|
||||
cache = hass[cacheKey] = {};
|
||||
}
|
||||
|
||||
const lastResult = cache[entityId];
|
||||
|
||||
if (lastResult) {
|
||||
return lastResult;
|
||||
}
|
||||
|
||||
const result = func(hass, entityId, ...args);
|
||||
cache[entityId] = result;
|
||||
|
||||
result.then(
|
||||
// When successful, set timer to clear cache
|
||||
() =>
|
||||
setTimeout(() => {
|
||||
cache![entityId] = undefined;
|
||||
}, cacheTime),
|
||||
// On failure, clear cache right away
|
||||
() => {
|
||||
cache![entityId] = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
@@ -1,43 +1,80 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
interface ResultCache<T> {
|
||||
[entityId: string]: Promise<T> | undefined;
|
||||
interface CacheResult<T> {
|
||||
result: T;
|
||||
cacheKey: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches a result of a promise for X time. Allows optional extra validation
|
||||
* check to invalidate the cache.
|
||||
* @param cacheKey the key to store the cache
|
||||
* @param cacheTime the time to cache the result
|
||||
* @param func the function to fetch the data
|
||||
* @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key.
|
||||
* @param hass Home Assistant object
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
*/
|
||||
export const timeCachePromiseFunc = async <T>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
|
||||
func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
|
||||
generateCacheKey:
|
||||
| ((hass: HomeAssistant, lastResult: T) => unknown)
|
||||
| undefined,
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
|
||||
const anyHass = hass as any;
|
||||
const lastResult: Promise<CacheResult<T>> | CacheResult<T> | undefined =
|
||||
anyHass[cacheKey];
|
||||
|
||||
if (!cache) {
|
||||
cache = hass[cacheKey] = {};
|
||||
}
|
||||
const checkCachedResult = (result: CacheResult<T>): T | Promise<T> => {
|
||||
if (
|
||||
!generateCacheKey ||
|
||||
generateCacheKey(hass, result.result) === result.cacheKey
|
||||
) {
|
||||
return result.result;
|
||||
}
|
||||
|
||||
const lastResult = cache[entityId];
|
||||
anyHass[cacheKey] = undefined;
|
||||
return timeCachePromiseFunc(
|
||||
cacheKey,
|
||||
cacheTime,
|
||||
func,
|
||||
generateCacheKey,
|
||||
hass,
|
||||
...args
|
||||
);
|
||||
};
|
||||
|
||||
// If we have a cached result, return it if it's still valid
|
||||
if (lastResult) {
|
||||
return lastResult;
|
||||
return lastResult instanceof Promise
|
||||
? lastResult.then(checkCachedResult)
|
||||
: checkCachedResult(lastResult);
|
||||
}
|
||||
|
||||
const result = func(hass, entityId, ...args);
|
||||
cache[entityId] = result;
|
||||
const resultPromise = func(hass, ...args);
|
||||
anyHass[cacheKey] = resultPromise;
|
||||
|
||||
result.then(
|
||||
resultPromise.then(
|
||||
// When successful, set timer to clear cache
|
||||
() =>
|
||||
(result) => {
|
||||
anyHass[cacheKey] = {
|
||||
result,
|
||||
cacheKey: generateCacheKey?.(hass, result),
|
||||
};
|
||||
setTimeout(() => {
|
||||
cache![entityId] = undefined;
|
||||
}, cacheTime),
|
||||
anyHass[cacheKey] = undefined;
|
||||
}, cacheTime);
|
||||
},
|
||||
// On failure, clear cache right away
|
||||
() => {
|
||||
cache![entityId] = undefined;
|
||||
anyHass[cacheKey] = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
return resultPromise;
|
||||
};
|
||||
|
@@ -86,6 +86,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
@@ -269,6 +271,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
item-value-path="id"
|
||||
item-label-path="name"
|
||||
@opened-changed=${this._openedChanged}
|
||||
|
@@ -11,6 +11,8 @@ class HaDevicesPicker extends LitElement {
|
||||
|
||||
@property() public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
@@ -66,6 +68,7 @@ class HaDevicesPicker extends LitElement {
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.label=${this.pickDeviceLabel}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._addDevice}
|
||||
></ha-device-picker>
|
||||
</div>
|
||||
|
@@ -14,6 +14,8 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {string}
|
||||
@@ -108,6 +110,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.label=${this.pickEntityLabel}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._addEntity}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
|
@@ -19,6 +19,8 @@ class HaEntityAttributePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@@ -61,6 +63,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
)}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
|
@@ -18,7 +18,6 @@ import "./state-badge";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
friendly_name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
@@ -40,6 +39,8 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@@ -97,9 +98,6 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ attribute: "item-value-path" }) public itemValuePath =
|
||||
"entity_id";
|
||||
|
||||
@property({ type: Boolean }) public hideClearIcon = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
@@ -148,7 +146,6 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
id: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_entities"
|
||||
@@ -169,15 +166,10 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => {
|
||||
const stateObj = hass!.states[key];
|
||||
|
||||
return {
|
||||
...stateObj,
|
||||
friendly_name: computeStateName(stateObj) || key,
|
||||
id: stateObj.context.id,
|
||||
};
|
||||
})
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
@@ -205,15 +197,10 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
states = entityIds
|
||||
.map((key) => {
|
||||
const stateObj = hass!.states[key];
|
||||
|
||||
return {
|
||||
...stateObj,
|
||||
friendly_name: computeStateName(stateObj) || key,
|
||||
id: stateObj.context?.id,
|
||||
};
|
||||
})
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
@@ -258,7 +245,6 @@ export class HaEntityPicker extends LitElement {
|
||||
state: "",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
id: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: this.hass!.localize(
|
||||
"ui.components.entity.entity-picker.no_match"
|
||||
@@ -311,8 +297,8 @@ export class HaEntityPicker extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="entity_id"
|
||||
item-label-path="friendly_name"
|
||||
.itemValuePath=${this.itemValuePath}
|
||||
.hass=${this.hass}
|
||||
.value=${this._value}
|
||||
.label=${this.label === undefined
|
||||
@@ -321,6 +307,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
.filteredItems=${this._states}
|
||||
.renderer=${rowRenderer}
|
||||
.required=${this.required}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
|
@@ -30,6 +30,8 @@ class HaAddonPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
||||
|
||||
public open() {
|
||||
@@ -55,6 +57,8 @@ class HaAddonPicker extends LitElement {
|
||||
? this.hass.localize("ui.components.addon-picker.addon")
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.renderer=${rowRenderer}
|
||||
.items=${this._addons}
|
||||
item-value-path="slug"
|
||||
|
@@ -28,8 +28,8 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@@ -84,6 +84,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
@@ -315,6 +317,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-label-path="name"
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.area-picker.area")
|
||||
: this.label}
|
||||
|
163
src/components/ha-areas-picker.ts
Normal file
163
src/components/ha-areas-picker.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { EntityRegistryEntry } from "../data/entity_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-area-picker";
|
||||
|
||||
@customElement("ha-areas-picker")
|
||||
export class HaAreasPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[];
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd?: boolean;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@property({ attribute: "picked-area-label" })
|
||||
public pickedAreaLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-area-label" })
|
||||
public pickAreaLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const currentAreas = this._currentAreas;
|
||||
return html`
|
||||
${currentAreas.map(
|
||||
(area) => html`
|
||||
<div>
|
||||
<ha-area-picker
|
||||
.curValue=${area}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${area}
|
||||
.label=${this.pickedAreaLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._areaChanged}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-area-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickAreaLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._addArea}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentAreas(): string[] {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateAreas(areas) {
|
||||
this.value = areas;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: areas,
|
||||
});
|
||||
}
|
||||
|
||||
private _areaChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const curValue = (ev.currentTarget as any).curValue;
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue === curValue) {
|
||||
return;
|
||||
}
|
||||
const currentAreas = this._currentAreas;
|
||||
if (!newValue || currentAreas.includes(newValue)) {
|
||||
this._updateAreas(currentAreas.filter((ent) => ent !== curValue));
|
||||
return;
|
||||
}
|
||||
this._updateAreas(
|
||||
currentAreas.map((ent) => (ent === curValue ? newValue : ent))
|
||||
);
|
||||
}
|
||||
|
||||
private _addArea(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const toAdd = ev.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as any).value = "";
|
||||
const currentAreas = this._currentAreas;
|
||||
if (currentAreas.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateAreas([...currentAreas, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-areas-picker": HaAreasPicker;
|
||||
}
|
||||
}
|
@@ -1,12 +1,13 @@
|
||||
import { LitElement, html, TemplateResult, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-select";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "./ha-textfield";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface TimeChangedEvent {
|
||||
days?: number;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
@@ -21,6 +22,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property() label?: string;
|
||||
|
||||
/**
|
||||
* Helper for the input
|
||||
*/
|
||||
@property() helper?: string;
|
||||
|
||||
/**
|
||||
* auto validate time inputs
|
||||
*/
|
||||
@@ -41,6 +47,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/**
|
||||
* day
|
||||
*/
|
||||
@property({ type: Number }) days = 0;
|
||||
|
||||
/**
|
||||
* hour
|
||||
*/
|
||||
@@ -61,6 +72,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Number }) milliseconds = 0;
|
||||
|
||||
/**
|
||||
* Label for the day input
|
||||
*/
|
||||
@property() dayLabel = "";
|
||||
|
||||
/**
|
||||
* Label for the hour input
|
||||
*/
|
||||
@@ -91,6 +107,11 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property({ type: Boolean }) enableMillisecond = false;
|
||||
|
||||
/**
|
||||
* show the day field
|
||||
*/
|
||||
@property({ type: Boolean }) enableDay = false;
|
||||
|
||||
/**
|
||||
* limit hours input
|
||||
*/
|
||||
@@ -108,8 +129,33 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : ""}
|
||||
${this.label
|
||||
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
|
||||
: ""}
|
||||
<div class="time-input-wrap">
|
||||
${this.enableDay
|
||||
? html`
|
||||
<ha-textfield
|
||||
id="day"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.days}
|
||||
.label=${this.dayLabel}
|
||||
name="days"
|
||||
@input=${this._valueChanged}
|
||||
@focus=${this._onFocus}
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
id="hour"
|
||||
type="number"
|
||||
@@ -207,6 +253,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
<mwc-list-item value="PM">PM</mwc-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -303,6 +350,13 @@ export class HaBaseTimeInput extends LitElement {
|
||||
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.helper {
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: 0.75rem;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -25,13 +25,7 @@ export class HaChipSet extends LitElement {
|
||||
${unsafeCSS(chipStyles)}
|
||||
|
||||
slot::slotted(ha-chip) {
|
||||
margin: 4px;
|
||||
}
|
||||
slot::slotted(ha-chip:first-of-type) {
|
||||
margin-left: -4px;
|
||||
}
|
||||
slot::slotted(ha-chip:last-of-type) {
|
||||
margin-right: -4px;
|
||||
margin: 4px 4px 4px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaChip extends LitElement {
|
||||
@property({ type: Boolean }) public hasIcon = false;
|
||||
|
||||
@property({ type: Boolean }) public hasTrailingIcon = false;
|
||||
|
||||
@property({ type: Boolean }) public noText = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -30,6 +32,11 @@ export class HaChip extends LitElement {
|
||||
<span class="mdc-chip__text"><slot></slot></span>
|
||||
</span>
|
||||
</span>
|
||||
${this.hasTrailingIcon
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--trailing">
|
||||
<slot name="trailing-icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -53,14 +60,20 @@ export class HaChip extends LitElement {
|
||||
color: var(--ha-chip-text-color, var(--primary-text-color));
|
||||
}
|
||||
|
||||
.mdc-chip__icon--leading {
|
||||
--mdc-icon-size: 20px;
|
||||
.mdc-chip__icon--leading,
|
||||
.mdc-chip__icon--trailing {
|
||||
--mdc-icon-size: 18px;
|
||||
line-height: 14px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip.no-text
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
span[role="gridcell"] {
|
||||
line-height: 14px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -87,6 +87,8 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "opened" })
|
||||
private _opened?: boolean;
|
||||
|
||||
@@ -108,17 +110,22 @@ export class HaComboBox extends LitElement {
|
||||
return this._comboBox.selectedItem;
|
||||
}
|
||||
|
||||
public setInputValue(value: string) {
|
||||
this._comboBox.value = value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<vaadin-combo-box-light
|
||||
.itemValuePath=${this.itemValuePath}
|
||||
.itemIdPath=${this.itemIdPath}
|
||||
.itemLabelPath=${this.itemLabelPath}
|
||||
.value=${this.value || ""}
|
||||
.items=${this.items}
|
||||
.value=${this.value || ""}
|
||||
.filteredItems=${this.filteredItems}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@@ -129,6 +136,7 @@ export class HaComboBox extends LitElement {
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.errorMessage=${this.errorMessage}
|
||||
class="input"
|
||||
|
@@ -35,6 +35,8 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
render() {
|
||||
@@ -46,6 +48,7 @@ export class HaDateInput extends LitElement {
|
||||
.value=${this.value
|
||||
? formatDateNumeric(new Date(this.value), this.locale)
|
||||
: ""}
|
||||
.required=${this.required}
|
||||
>
|
||||
<ha-svg-icon slot="trailingIcon" .path=${mdiCalendar}></ha-svg-icon>
|
||||
</ha-textfield>`;
|
||||
|
@@ -5,6 +5,7 @@ import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
@@ -17,10 +18,14 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public enableMillisecond?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public enableDay?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("paper-time-input", true) private _input?: HTMLElement;
|
||||
@@ -35,19 +40,23 @@ class HaDurationInput extends LitElement {
|
||||
return html`
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
enableSecond
|
||||
.enableMillisecond=${this.enableMillisecond}
|
||||
.enableDay=${this.enableDay}
|
||||
format="24"
|
||||
.days=${this._days}
|
||||
.hours=${this._hours}
|
||||
.minutes=${this._minutes}
|
||||
.seconds=${this._seconds}
|
||||
.milliseconds=${this._milliseconds}
|
||||
@value-changed=${this._durationChanged}
|
||||
noHoursLimit
|
||||
dayLabel="dd"
|
||||
hourLabel="hh"
|
||||
minLabel="mm"
|
||||
secLabel="ss"
|
||||
@@ -56,6 +65,10 @@ class HaDurationInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days ? Number(this.data.days) : 0;
|
||||
}
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours ? Number(this.data.hours) : 0;
|
||||
}
|
||||
@@ -94,6 +107,11 @@ class HaDurationInput extends LitElement {
|
||||
value.minutes %= 60;
|
||||
}
|
||||
|
||||
if (this.enableDay && value.hours > 24) {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
|
@@ -34,12 +34,25 @@ export const computeInitialHaFormData = (
|
||||
};
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
if ("boolean" in selector) {
|
||||
|
||||
if ("device" in selector) {
|
||||
data[field.name] = selector.device.multiple ? [] : "";
|
||||
} else if ("entity" in selector) {
|
||||
data[field.name] = selector.entity.multiple ? [] : "";
|
||||
} else if ("area" in selector) {
|
||||
data[field.name] = selector.area.multiple ? [] : "";
|
||||
} else if ("boolean" in selector) {
|
||||
data[field.name] = false;
|
||||
} else if ("text" in selector) {
|
||||
} else if (
|
||||
"text" in selector ||
|
||||
"addon" in selector ||
|
||||
"attribute" in selector ||
|
||||
"icon" in selector ||
|
||||
"theme" in selector
|
||||
) {
|
||||
data[field.name] = "";
|
||||
} else if ("number" in selector) {
|
||||
data[field.name] = "min" in selector.number ? selector.number.min : 0;
|
||||
data[field.name] = selector.number.min ?? 0;
|
||||
} else if ("select" in selector) {
|
||||
if (selector.select.options.length) {
|
||||
data[field.name] = selector.select.options[0][0];
|
||||
@@ -50,6 +63,23 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if ("time" in selector) {
|
||||
data[field.name] = "00:00:00";
|
||||
} else if ("date" in selector || "datetime" in selector) {
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
data[field.name] = `${now} 00:00:00`;
|
||||
} else if ("color_rgb" in selector) {
|
||||
data[field.name] = [0, 0, 0];
|
||||
} else if ("color_temp" in selector) {
|
||||
data[field.name] = selector.color_temp.min_mireds ?? 153;
|
||||
} else if (
|
||||
"action" in selector ||
|
||||
"media" in selector ||
|
||||
"target" in selector
|
||||
) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error("Selector not supported in initial form data");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -28,6 +28,8 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
@@ -53,6 +55,8 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
|
@@ -39,6 +39,8 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
@@ -56,6 +58,7 @@ export class HaIconPicker extends LitElement {
|
||||
.filteredItems=${iconItems}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.placeholder=${this.placeholder}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
|
@@ -33,7 +33,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="title">[[caption]]</div>
|
||||
<div class="title">[[_getTitle()]]</div>
|
||||
<div class="extra-container"><slot name="extra"></slot></div>
|
||||
<div class="slider-container">
|
||||
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
|
||||
@@ -49,10 +49,15 @@ class HaLabeledSlider extends PolymerElement {
|
||||
`;
|
||||
}
|
||||
|
||||
_getTitle() {
|
||||
return `${this.caption}${this.required ? "*" : ""}`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
caption: String,
|
||||
disabled: Boolean,
|
||||
required: Boolean,
|
||||
min: Number,
|
||||
max: Number,
|
||||
pin: Boolean,
|
||||
|
@@ -2,6 +2,11 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-markdown-element";
|
||||
|
||||
// Import components that are allwoed to be defined.
|
||||
import "./ha-alert";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-markdown")
|
||||
export class HaMarkdown extends LitElement {
|
||||
@property() public content?;
|
||||
|
@@ -14,11 +14,17 @@ export class HaAddonSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-addon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
></ha-addon-picker>`;
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-area-picker";
|
||||
import "../ha-areas-picker";
|
||||
|
||||
@customElement("ha-selector-area")
|
||||
export class HaAreaSelector extends LitElement {
|
||||
@@ -21,6 +22,8 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
@@ -38,21 +41,45 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-area-picker>`;
|
||||
if (!this.selector.area.multiple) {
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-areas-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.pickAreaLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-areas-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { AttributeSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
@@ -18,6 +18,8 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property() public context?: {
|
||||
filter_entity?: string;
|
||||
};
|
||||
@@ -31,6 +33,7 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
></ha-entity-attribute-picker>
|
||||
`;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorRGBSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorRGBSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-textfield";
|
||||
|
||||
@customElement("ha-selector-color_rgb")
|
||||
@@ -18,12 +18,16 @@ export class HaColorRGBSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
type="color"
|
||||
.value=${this.value ? rgb2hex(this.value as any) : ""}
|
||||
.label=${this.label || ""}
|
||||
.required=${this.required}
|
||||
.disalbled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
`;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labeled-slider";
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
@@ -17,6 +17,8 @@ export class HaColorTempSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-labeled-slider
|
||||
@@ -26,6 +28,8 @@ export class HaColorTempSelector extends LitElement {
|
||||
.min=${this.selector.color_temp.min_mireds ?? 153}
|
||||
.max=${this.selector.color_temp.max_mireds ?? 500}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@change=${this._valueChanged}
|
||||
></ha-labeled-slider>
|
||||
`;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
@@ -16,6 +16,8 @@ export class HaDateSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-date-input
|
||||
@@ -23,6 +25,7 @@ export class HaDateSelector extends LitElement {
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
>
|
||||
</ha-date-input>
|
||||
`;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-datetime")
|
||||
export class HaDateTimeSelector extends LitElement {
|
||||
@@ -20,6 +20,8 @@ export class HaDateTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-date-input") private _dateInput!: HaDateInput;
|
||||
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
@@ -32,6 +34,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${values?.[0]}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -41,6 +44,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
.value=${values?.[1] || "0:00:00"}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-time-input>
|
||||
`;
|
||||
|
@@ -21,6 +21,8 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
@@ -36,20 +38,23 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
if (!this.selector.device.multiple) {
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-device-picker> `;
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.includeDeviceClasses=${this.selector.device.entity?.device_class
|
||||
? [this.selector.device.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
></ha-device-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -63,6 +68,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.required=${this.required}
|
||||
></ha-devices-picker>
|
||||
`;
|
||||
}
|
||||
|
@@ -14,6 +14,8 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
@@ -22,9 +24,11 @@ export class HaTimeDuration extends LitElement {
|
||||
return html`
|
||||
<ha-duration-input
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.data=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.enableDay=${this.selector.duration.enable_day}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,21 +1,23 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
|
||||
@customElement("ha-selector-entity")
|
||||
export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
export class HaEntitySelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public selector!: EntitySelector;
|
||||
|
||||
@state() private _entityPlaformLookup?: Record<string, string>;
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@@ -23,6 +25,8 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
if (!this.selector.entity.multiple) {
|
||||
return html`<ha-entity-picker
|
||||
@@ -33,7 +37,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.itemValuePath=${!this.selector.entity.use_uuid ? "entity_id" : "id"}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
@@ -46,53 +50,52 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.required=${this.required}
|
||||
></ha-entities-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
}),
|
||||
];
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
changedProps.has("selector") &&
|
||||
this.selector.entity.integration &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (this.selector.entity?.domain) {
|
||||
const filterDomain = this.selector.entity.domain;
|
||||
const filterDomainIsArray = Array.isArray(filterDomain);
|
||||
const {
|
||||
domain: filterDomain,
|
||||
device_class: filterDeviceClass,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.entity;
|
||||
|
||||
if (filterDomain) {
|
||||
const entityDomain = computeStateDomain(entity);
|
||||
if (
|
||||
(filterDomainIsArray && !filterDomain.includes(entityDomain)) ||
|
||||
(!filterDomainIsArray && entityDomain !== filterDomain)
|
||||
Array.isArray(filterDomain)
|
||||
? !filterDomain.includes(entityDomain)
|
||||
: entityDomain !== filterDomain
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.selector.entity?.device_class) {
|
||||
if (
|
||||
!entity.attributes.device_class ||
|
||||
entity.attributes.device_class !== this.selector.entity.device_class
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filterDeviceClass &&
|
||||
entity.attributes.device_class !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.entity?.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
this.selector.entity.integration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import "../ha-icon-picker";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { IconSelector } from "../../data/selector";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { IconSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-picker";
|
||||
|
||||
@customElement("ha-selector-icon")
|
||||
export class HaIconSelector extends LitElement {
|
||||
@@ -17,11 +17,15 @@ export class HaIconSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.fallbackPath=${this.selector.icon.fallbackPath}
|
||||
.placeholder=${this.selector.icon.placeholder}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
@@ -6,7 +6,6 @@ import type {
|
||||
LocationSelector,
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
@@ -52,7 +51,10 @@ export class HaLocationSelector extends LitElement {
|
||||
longitude: value?.longitude || this.hass.config.longitude,
|
||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
||||
radius_color: zoneRadiusColor,
|
||||
icon: selector.location.icon,
|
||||
icon:
|
||||
selector.location.icon || selector.location.radius
|
||||
? "mdi:map-marker-radius"
|
||||
: "mdi:map-marker",
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
},
|
||||
|
@@ -35,6 +35,8 @@ export class HaMediaSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public required = true;
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
willUpdate(changedProps: PropertyValues<this>) {
|
||||
@@ -84,6 +86,7 @@ export class HaMediaSelector extends LitElement {
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
include-domains='["media_player"]'
|
||||
allow-custom-entity
|
||||
@value-changed=${this._entityChanged}
|
||||
|
@@ -19,13 +19,15 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`${this.selector.number.mode !== "box"
|
||||
? html`${this.label}<ha-slider
|
||||
? html`${this.label}${this.required ? "*" : ""}<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
@@ -48,6 +50,8 @@ export class HaNumberSelector extends LitElement {
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this.value ?? ""}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
helperPersistent
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.suffix=${this.selector.number.unit_of_measurement}
|
||||
|
@@ -16,10 +16,13 @@ export class HaObjectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.readonly=${this.disabled}
|
||||
.required=${this.required}
|
||||
.placeholder=${this.placeholder}
|
||||
.defaultValue=${this.value}
|
||||
@value-changed=${this._handleChange}
|
||||
|
@@ -1,13 +1,19 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-chip";
|
||||
import "../ha-chip-set";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-formfield";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@@ -15,7 +21,7 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -25,30 +31,116 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||
|
||||
private _filter = "";
|
||||
|
||||
protected render() {
|
||||
if (this.required && this.selector.select.options!.length < 6) {
|
||||
const options = this.selector.select.options.map((option) =>
|
||||
typeof option === "object" ? option : { value: option, label: option }
|
||||
);
|
||||
|
||||
if (!this.selector.select.custom_value && this._mode === "list") {
|
||||
if (!this.selector.select.multiple || this.required) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<mwc-formfield .label=${item.label}>
|
||||
<ha-radio
|
||||
.checked=${item.value === this.value}
|
||||
.value=${item.value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`
|
||||
<mwc-formfield .label=${label}>
|
||||
<ha-radio
|
||||
.checked=${value === this.value}
|
||||
.value=${value}
|
||||
${this.label}${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-formfield .label=${item.label}>
|
||||
<ha-checkbox
|
||||
.checked=${this.value?.includes(item.value)}
|
||||
.value=${item.value}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
})}
|
||||
@change=${this._checkboxChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.selector.select.multiple) {
|
||||
const value =
|
||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||
|
||||
return html`
|
||||
<ha-chip-set>
|
||||
${value?.map(
|
||||
(item, idx) =>
|
||||
html`
|
||||
<ha-chip hasTrailingIcon>
|
||||
${options.find((option) => option.value === item)?.label ||
|
||||
item}
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiClose}
|
||||
.idx=${idx}
|
||||
@click=${this._removeItem}
|
||||
></ha-svg-icon>
|
||||
</ha-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${this._filter}
|
||||
.items=${options.filter((item) => !this.value?.includes(item.value))}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.selector.select.custom_value) {
|
||||
if (
|
||||
this.value !== undefined &&
|
||||
!options.find((option) => option.value === this.value)
|
||||
) {
|
||||
options.unshift({ value: this.value, label: this.value });
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.items=${options}
|
||||
.value=${this.value}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
@@ -60,36 +152,131 @@ export class HaSelectSelector extends LitElement {
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
>
|
||||
${this.selector.select.options.map((item: string | SelectOption) => {
|
||||
const value = typeof item === "object" ? item.value : item;
|
||||
const label = typeof item === "object" ? item.label : item;
|
||||
|
||||
return html`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
|
||||
})}
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<mwc-list-item .value=${item.value}>${item.label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _mode(): "list" | "dropdown" {
|
||||
return (
|
||||
this.selector.select.mode ||
|
||||
(this.selector.select.options.length < 6 ? "list" : "dropdown")
|
||||
);
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
if (this.disabled || !ev.target.value) {
|
||||
const value = ev.detail?.value || ev.target.value;
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: ev.target.value,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
private _checkboxChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newValue: string[];
|
||||
const value: string = ev.target.value;
|
||||
const checked = ev.target.checked;
|
||||
|
||||
if (checked) {
|
||||
if (!this.value) {
|
||||
newValue = [value];
|
||||
} else if (this.value.includes(value)) {
|
||||
return;
|
||||
} else {
|
||||
newValue = [...this.value, value];
|
||||
}
|
||||
mwc-formfield {
|
||||
display: block;
|
||||
} else {
|
||||
if (!this.value?.includes(value)) {
|
||||
return;
|
||||
}
|
||||
`;
|
||||
newValue = (this.value as string[]).filter((v) => v !== value);
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private async _removeItem(ev) {
|
||||
const value: string[] = [...(this.value! as string[])];
|
||||
value.splice(ev.target.idx, 1);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value,
|
||||
});
|
||||
await this.updateComplete;
|
||||
this._filterChanged();
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (this.disabled || newValue === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selector.select.multiple) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== undefined && this.value?.includes(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._filterChanged();
|
||||
this.comboBox.setInputValue("");
|
||||
}, 0);
|
||||
|
||||
const currentValue =
|
||||
!this.value || this.value === "" ? [] : (this.value as string[]);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: [...currentValue, newValue],
|
||||
});
|
||||
}
|
||||
|
||||
private _filterChanged(ev?: CustomEvent): void {
|
||||
this._filter = ev?.detail.value || "";
|
||||
|
||||
const filteredItems = this.comboBox.items?.filter((item) => {
|
||||
if (this.selector.select.multiple && this.value?.includes(item.value)) {
|
||||
return false;
|
||||
}
|
||||
const label = item.label || item.value;
|
||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||
});
|
||||
|
||||
if (this._filter && this.selector.select.custom_value) {
|
||||
filteredItems?.unshift({ label: this._filter, value: this._filter });
|
||||
}
|
||||
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select,
|
||||
mwc-formfield,
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -18,6 +18,8 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public selector!: StringSelector;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -32,6 +34,8 @@ export class HaTextSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
@input=${this._handleChange}
|
||||
autocapitalize="none"
|
||||
@@ -44,6 +48,8 @@ export class HaTextSelector extends LitElement {
|
||||
return html`<ha-textfield
|
||||
.value=${this.value || ""}
|
||||
.placeholder=${this.placeholder || ""}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
|
||||
@input=${this._handleChange}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ThemeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-theme-picker";
|
||||
|
||||
@customElement("ha-selector-theme")
|
||||
export class HaThemeSelector extends LitElement {
|
||||
@@ -16,13 +16,17 @@ export class HaThemeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
></hui-theme-select-editor>
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-theme-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -16,12 +16,15 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${this.value}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label}
|
||||
enable-second
|
||||
></ha-time-input>
|
||||
|
@@ -471,6 +471,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
@@ -489,9 +490,6 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ export class HaSettingsRow extends LitElement {
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="content"><slot></slot></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,18 @@ export class HaSettingsRow extends LitElement {
|
||||
);
|
||||
flex: 1;
|
||||
}
|
||||
.content {
|
||||
display: contents;
|
||||
}
|
||||
:host(:not([narrow])) .content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.content ::slotted(*) {
|
||||
width: var(--settings-row-content-width);
|
||||
}
|
||||
:host([narrow]) {
|
||||
align-items: normal;
|
||||
flex-direction: column;
|
||||
|
@@ -37,6 +37,7 @@ import { LocalStorage } from "../common/decorators/local-storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { ActionHandlerDetail } from "../data/lovelace";
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import { updateCanInstall, UpdateEntity } from "../data/update";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -68,7 +70,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
|
||||
const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
@@ -190,6 +191,8 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _updatesCount = 0;
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
@@ -235,6 +238,7 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -290,6 +294,12 @@ class HaSidebar extends LitElement {
|
||||
toggleAttribute(this, "rtl", computeRTL(this.hass));
|
||||
}
|
||||
|
||||
this._updatesCount = Object.values(this.hass.states).filter(
|
||||
(entity) =>
|
||||
computeStateDomain(entity) === "update" &&
|
||||
updateCanInstall(entity as UpdateEntity)
|
||||
).length;
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
return;
|
||||
}
|
||||
@@ -387,35 +397,37 @@ class HaSidebar extends LitElement {
|
||||
icon?: string | null,
|
||||
iconPath?: string | null
|
||||
) {
|
||||
return html`
|
||||
<a
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
</paper-icon-item>
|
||||
${this.editMode
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.hide_panel")}
|
||||
.path=${mdiClose}
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
</a>
|
||||
`;
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title)
|
||||
: html`
|
||||
<a
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${iconPath}
|
||||
></ha-svg-icon>`
|
||||
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text">${title}</span>
|
||||
</paper-icon-item>
|
||||
${this.editMode
|
||||
? html`<ha-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.hide_panel")}
|
||||
.path=${mdiClose}
|
||||
class="hide-panel"
|
||||
.panel=${urlPath}
|
||||
@click=${this._hidePanel}
|
||||
></ha-icon-button>`
|
||||
: ""}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
@@ -477,6 +489,35 @@ class HaSidebar extends LitElement {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null) {
|
||||
return html` <a
|
||||
class="configuration-container"
|
||||
role="option"
|
||||
href="/config"
|
||||
data-panel="config"
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item class="configuration" role="option">
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand && this._updatesCount > 0
|
||||
? html`
|
||||
<span class="configuration-badge" slot="item-icon">
|
||||
${this._updatesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">${title}</span>
|
||||
${this.alwaysExpand && this._updatesCount > 0
|
||||
? html`
|
||||
<span class="configuration-badge">${this._updatesCount}</span>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderNotifications() {
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
@@ -953,18 +994,21 @@ class HaSidebar extends LitElement {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
.notifications-container {
|
||||
.notifications-container,
|
||||
.configuration-container {
|
||||
display: flex;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
}
|
||||
:host([rtl]) .notifications-container {
|
||||
:host([rtl]) .notifications-container,
|
||||
:host([rtl]) .configuration-container {
|
||||
margin-left: initial;
|
||||
margin-right: env(safe-area-inset-right);
|
||||
}
|
||||
.notifications {
|
||||
cursor: pointer;
|
||||
}
|
||||
.notifications .item-text {
|
||||
.notifications .item-text,
|
||||
.configuration .item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.profile {
|
||||
@@ -988,7 +1032,8 @@ class HaSidebar extends LitElement {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
.notification-badge,
|
||||
.configuration-badge {
|
||||
min-width: 20px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
@@ -999,7 +1044,11 @@ class HaSidebar extends LitElement {
|
||||
padding: 0px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
}
|
||||
ha-svg-icon + .notification-badge {
|
||||
.configuration-badge {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
ha-svg-icon + .notification-badge,
|
||||
ha-svg-icon + .configuration-badge {
|
||||
position: absolute;
|
||||
bottom: 14px;
|
||||
left: 26px;
|
||||
|
@@ -19,13 +19,11 @@ export class HaTextArea extends TextAreaBase {
|
||||
textfieldStyles,
|
||||
textareaStyles,
|
||||
css`
|
||||
:host([autogrow]) {
|
||||
max-height: 200px;
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field {
|
||||
position: relative;
|
||||
min-height: 74px;
|
||||
min-width: 178px;
|
||||
max-height: 200px;
|
||||
}
|
||||
:host([autogrow]) .mdc-text-field:after {
|
||||
content: attr(data-value);
|
||||
|
@@ -2,29 +2,31 @@ import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import "../../../components/ha-select";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("hui-theme-select-editor")
|
||||
export class HuiThemeSelectEditor extends LitElement {
|
||||
@customElement("ha-theme-picker")
|
||||
export class HaThemePicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
this.hass!.localize("ui.components.theme_picker.theme")}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
@@ -32,7 +34,7 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
>
|
||||
<mwc-list-item value="remove"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.no_theme"
|
||||
"ui.components.theme_picker.no_theme"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
@@ -64,6 +66,6 @@ export class HuiThemeSelectEditor extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-theme-select-editor": HuiThemeSelectEditor;
|
||||
"ha-theme-picker": HaThemePicker;
|
||||
}
|
||||
}
|
@@ -2,8 +2,8 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
@@ -16,6 +16,8 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-second" })
|
||||
public enableSecond = false;
|
||||
|
||||
@@ -43,6 +45,7 @@ export class HaTimeInput extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._timeChanged}
|
||||
.enableSecond=${this.enableSecond}
|
||||
.required=${this.required}
|
||||
></ha-base-time-input>
|
||||
`;
|
||||
}
|
||||
|
@@ -33,6 +33,8 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
public setValue(value): void {
|
||||
@@ -59,7 +61,7 @@ export class HaYamlEditor extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${this.label ? html`<p>${this.label}</p>` : ""}
|
||||
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
|
||||
<ha-code-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
|
@@ -488,6 +488,14 @@ export class HaMap extends ReactiveElement {
|
||||
text-align: center;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.leaflet-pane {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
.leaflet-control,
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -62,6 +62,7 @@ export interface ContextConstraint {
|
||||
export interface BaseTrigger {
|
||||
platform: string;
|
||||
id?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
|
36
src/data/backup.ts
Normal file
36
src/data/backup.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface BackupContent {
|
||||
slug: string;
|
||||
date: string;
|
||||
name: string;
|
||||
size: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface BackupData {
|
||||
backing_up: boolean;
|
||||
backups: BackupContent[];
|
||||
}
|
||||
|
||||
export const getBackupDownloadUrl = (slug: string) =>
|
||||
`/api/backup/download/${slug}`;
|
||||
|
||||
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
|
||||
hass.callWS({
|
||||
type: "backup/info",
|
||||
});
|
||||
|
||||
export const removeBackup = (
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "backup/remove",
|
||||
slug,
|
||||
});
|
||||
|
||||
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
|
||||
hass.callWS({
|
||||
type: "backup/generate",
|
||||
});
|
@@ -2,7 +2,7 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
||||
import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { getSignedPath } from "./auth";
|
||||
|
||||
@@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async (
|
||||
width: number,
|
||||
height: number
|
||||
) => {
|
||||
const base_url = await timeCachePromiseFunc(
|
||||
const base_url = await timeCacheEntityPromiseFunc(
|
||||
"_cameraTmbUrl",
|
||||
9000,
|
||||
fetchThumbnailUrl,
|
||||
|
@@ -41,6 +41,10 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
disabled_by?: string | null;
|
||||
hidden_by: string | null;
|
||||
new_entity_id?: string;
|
||||
options_domain?: string;
|
||||
options?: {
|
||||
unit_of_measurement?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const findBatteryEntity = (
|
||||
|
46
src/data/entity_sources.ts
Normal file
46
src/data/entity_sources.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface EntitySourceConfigEntry {
|
||||
source: "config_entry";
|
||||
domain: string;
|
||||
custom_component: boolean;
|
||||
config_entry: string;
|
||||
}
|
||||
|
||||
interface EntitySourcePlatformConfig {
|
||||
source: "platform_config";
|
||||
domain: string;
|
||||
custom_component: boolean;
|
||||
}
|
||||
|
||||
export type EntitySources = Record<
|
||||
string,
|
||||
EntitySourceConfigEntry | EntitySourcePlatformConfig
|
||||
>;
|
||||
|
||||
const fetchEntitySources = (
|
||||
hass: HomeAssistant,
|
||||
entity_id?: string
|
||||
): Promise<EntitySources> =>
|
||||
hass.callWS({
|
||||
type: "entity/source",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const fetchEntitySourcesWithCache = (
|
||||
hass: HomeAssistant,
|
||||
entity_id?: string
|
||||
): Promise<EntitySources> =>
|
||||
entity_id
|
||||
? fetchEntitySources(hass, entity_id)
|
||||
: timeCachePromiseFunc(
|
||||
"_entitySources",
|
||||
// cache for 30 seconds
|
||||
30000,
|
||||
fetchEntitySources,
|
||||
// We base the cache on number of states. If number of states
|
||||
// changes we force a refresh
|
||||
(hass2) => Object.keys(hass2.states).length,
|
||||
hass
|
||||
);
|
@@ -84,6 +84,8 @@ export interface StatisticsMetaData {
|
||||
statistic_id: string;
|
||||
source: string;
|
||||
name?: string | null;
|
||||
has_sum: boolean;
|
||||
has_mean: boolean;
|
||||
}
|
||||
|
||||
export type StatisticsValidationResult =
|
||||
|
@@ -16,6 +16,11 @@ import {
|
||||
mdiPlayPause,
|
||||
mdiPodcast,
|
||||
mdiPower,
|
||||
mdiRepeat,
|
||||
mdiRepeatOff,
|
||||
mdiRepeatOnce,
|
||||
mdiShuffle,
|
||||
mdiShuffleDisabled,
|
||||
mdiSkipNext,
|
||||
mdiSkipPrevious,
|
||||
mdiStop,
|
||||
@@ -49,6 +54,8 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||
entity_picture_local?: string;
|
||||
is_volume_muted?: boolean;
|
||||
volume_level?: number;
|
||||
repeat?: string;
|
||||
shuffle?: boolean;
|
||||
source?: string;
|
||||
source_list?: string[];
|
||||
sound_mode?: string;
|
||||
@@ -80,7 +87,9 @@ export const SUPPORT_VOLUME_BUTTONS = 1024;
|
||||
export const SUPPORT_SELECT_SOURCE = 2048;
|
||||
export const SUPPORT_STOP = 4096;
|
||||
export const SUPPORT_PLAY = 16384;
|
||||
export const SUPPORT_REPEAT_SET = 262144;
|
||||
export const SUPPORT_SELECT_SOUND_MODE = 65536;
|
||||
export const SUPPORT_SHUFFLE_SET = 32768;
|
||||
export const SUPPORT_BROWSE_MEDIA = 131072;
|
||||
|
||||
export type MediaPlayerBrowseAction = "pick" | "play";
|
||||
@@ -233,7 +242,8 @@ export const computeMediaDescription = (
|
||||
};
|
||||
|
||||
export const computeMediaControls = (
|
||||
stateObj: MediaPlayerEntity
|
||||
stateObj: MediaPlayerEntity,
|
||||
useExtendedControls = false
|
||||
): ControlButton[] | undefined => {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
@@ -266,6 +276,18 @@ export const computeMediaControls = (
|
||||
}
|
||||
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
const stateAttr = stateObj.attributes;
|
||||
|
||||
if (
|
||||
(state === "playing" || state === "paused" || assumedState) &&
|
||||
supportsFeature(stateObj, SUPPORT_SHUFFLE_SET) &&
|
||||
useExtendedControls
|
||||
) {
|
||||
buttons.push({
|
||||
icon: stateAttr.shuffle === true ? mdiShuffle : mdiShuffleDisabled,
|
||||
action: "shuffle_set",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" || state === "paused" || assumedState) &&
|
||||
@@ -337,6 +359,22 @@ export const computeMediaControls = (
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
(state === "playing" || state === "paused" || assumedState) &&
|
||||
supportsFeature(stateObj, SUPPORT_REPEAT_SET) &&
|
||||
useExtendedControls
|
||||
) {
|
||||
buttons.push({
|
||||
icon:
|
||||
stateAttr.repeat === "all"
|
||||
? mdiRepeat
|
||||
: stateAttr.repeat === "one"
|
||||
? mdiRepeatOnce
|
||||
: mdiRepeatOff,
|
||||
action: "repeat_set",
|
||||
});
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? buttons : undefined;
|
||||
};
|
||||
|
||||
@@ -375,3 +413,31 @@ export const setMediaPlayerVolume = (
|
||||
volume_level: number
|
||||
) =>
|
||||
hass.callService("media_player", "volume_set", { entity_id, volume_level });
|
||||
|
||||
export const handleMediaControlClick = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: MediaPlayerEntity,
|
||||
action: string
|
||||
) =>
|
||||
hass!.callService(
|
||||
"media_player",
|
||||
action,
|
||||
action === "shuffle_set"
|
||||
? {
|
||||
entity_id: stateObj!.entity_id,
|
||||
shuffle: !stateObj!.attributes.shuffle,
|
||||
}
|
||||
: action === "repeat_set"
|
||||
? {
|
||||
entity_id: stateObj!.entity_id,
|
||||
repeat:
|
||||
stateObj!.attributes.repeat === "all"
|
||||
? "one"
|
||||
: stateObj!.attributes.repeat === "off"
|
||||
? "all"
|
||||
: "off",
|
||||
}
|
||||
: {
|
||||
entity_id: stateObj!.entity_id,
|
||||
}
|
||||
);
|
||||
|
@@ -46,6 +46,7 @@ export interface AreaSelector {
|
||||
manufacturer?: DeviceSelector["device"]["manufacturer"];
|
||||
model?: DeviceSelector["device"]["model"];
|
||||
};
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,8 +97,9 @@ export interface DeviceSelector {
|
||||
}
|
||||
|
||||
export interface DurationSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
duration: {};
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EntitySelector {
|
||||
@@ -106,7 +108,6 @@ export interface EntitySelector {
|
||||
domain?: string | string[];
|
||||
device_class?: string;
|
||||
multiple?: boolean;
|
||||
use_uuid?: boolean;
|
||||
include_entities?: string[];
|
||||
exclude_entities?: string[];
|
||||
};
|
||||
@@ -169,6 +170,9 @@ export interface SelectOption {
|
||||
|
||||
export interface SelectSelector {
|
||||
select: {
|
||||
multiple?: boolean;
|
||||
custom_value?: boolean;
|
||||
mode?: "list" | "dropdown";
|
||||
options: string[] | SelectOption[];
|
||||
};
|
||||
}
|
||||
@@ -208,6 +212,7 @@ export interface TargetSelector {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ThemeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
theme: {};
|
||||
|
@@ -12,6 +12,7 @@ export type TimerEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
duration: string;
|
||||
remaining: string;
|
||||
restore: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,12 +27,14 @@ export interface Timer {
|
||||
name: string;
|
||||
icon?: string;
|
||||
duration?: string | number | DurationDict;
|
||||
restore?: boolean;
|
||||
}
|
||||
|
||||
export interface TimerMutableParams {
|
||||
name: string;
|
||||
icon: string;
|
||||
duration: string | number | DurationDict;
|
||||
restore: boolean;
|
||||
}
|
||||
|
||||
export const fetchTimer = (hass: HomeAssistant) =>
|
||||
|
@@ -3,11 +3,13 @@ import type {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
export const UPDATE_SUPPORT_PROGRESS = 4;
|
||||
export const UPDATE_SUPPORT_BACKUP = 8;
|
||||
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
|
||||
|
||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||
current_version: string | null;
|
||||
@@ -34,3 +36,9 @@ export const updateCanInstall = (entity: UpdateEntity): boolean =>
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||
|
||||
export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
|
||||
hass.callWS<string | null>({
|
||||
type: "update/release_notes",
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
@@ -14,9 +14,7 @@ import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-markdown";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
@@ -37,6 +35,7 @@ import { documentationUrl } from "../../util/documentation-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import {
|
||||
DataEntryFlowDialogParams,
|
||||
FlowHandlers,
|
||||
LoadingReason,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
import "./step-flow-abort";
|
||||
@@ -87,7 +86,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _handlers?: string[];
|
||||
@state() private _handlers?: FlowHandlers;
|
||||
|
||||
@state() private _handler?: string;
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { html } from "lit";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
createConfigFlow,
|
||||
deleteConfigFlow,
|
||||
@@ -23,17 +22,12 @@ export const showConfigFlowDialog = (
|
||||
showFlowDialog(element, dialogParams, {
|
||||
loadDevicesAndAreas: true,
|
||||
getFlowHandlers: async (hass) => {
|
||||
const [handlers] = await Promise.all([
|
||||
const [integrations, helpers] = await Promise.all([
|
||||
getConfigFlowHandlers(hass, "integration"),
|
||||
getConfigFlowHandlers(hass, "helper"),
|
||||
hass.loadBackendTranslation("title", undefined, true),
|
||||
]);
|
||||
|
||||
return handlers.sort((handlerA, handlerB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
domainToName(hass.localize, handlerA),
|
||||
domainToName(hass.localize, handlerB)
|
||||
)
|
||||
);
|
||||
return { integrations, helpers };
|
||||
},
|
||||
createFlow: async (hass, handler) => {
|
||||
const [step] = await Promise.all([
|
||||
@@ -91,6 +85,12 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.data_description.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.error.${error}`,
|
||||
|
@@ -12,10 +12,14 @@ import {
|
||||
} from "../../data/data_entry_flow";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export interface FlowHandlers {
|
||||
integrations: string[];
|
||||
helpers: string[];
|
||||
}
|
||||
export interface FlowConfig {
|
||||
loadDevicesAndAreas: boolean;
|
||||
|
||||
getFlowHandlers?: (hass: HomeAssistant) => Promise<string[]>;
|
||||
getFlowHandlers?: (hass: HomeAssistant) => Promise<FlowHandlers>;
|
||||
|
||||
createFlow(hass: HomeAssistant, handler: string): Promise<DataEntryFlowStep>;
|
||||
|
||||
@@ -50,6 +54,12 @@ export interface FlowConfig {
|
||||
field: HaFormSchema
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldHelper(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldError(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
|
@@ -89,6 +89,12 @@ export const showOptionsFlowDialog = (
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldError(hass, step, error) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.error.${error}`,
|
||||
|
@@ -54,6 +54,7 @@ class StepFlowForm extends LitElement {
|
||||
.schema=${step.data_schema}
|
||||
.error=${step.errors}
|
||||
.computeLabel=${this._labelCallback}
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
></ha-form>
|
||||
</div>
|
||||
@@ -166,6 +167,9 @@ class StepFlowForm extends LitElement {
|
||||
private _labelCallback = (field: HaFormSchema): string =>
|
||||
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
|
||||
|
||||
private _helperCallback = (field: HaFormSchema): string =>
|
||||
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
|
||||
|
||||
private _errorCallback = (error: string) =>
|
||||
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
|
||||
|
||||
|
@@ -26,11 +26,13 @@ import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { FlowHandlers } from "./show-dialog-data-entry-flow";
|
||||
|
||||
interface HandlerObj {
|
||||
name: string;
|
||||
slug: string;
|
||||
is_add?: boolean;
|
||||
is_helper?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -46,7 +48,7 @@ declare global {
|
||||
class StepFlowPickHandler extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public handlers!: string[];
|
||||
@property({ attribute: false }) public handlers!: FlowHandlers;
|
||||
|
||||
@property() public initialFilter?: string;
|
||||
|
||||
@@ -57,8 +59,12 @@ class StepFlowPickHandler extends LitElement {
|
||||
private _height?: number;
|
||||
|
||||
private _filterHandlers = memoizeOne(
|
||||
(h: string[], filter?: string, _localize?: LocalizeFunc) => {
|
||||
const handlers: HandlerObj[] = h.map((handler) => ({
|
||||
(
|
||||
h: FlowHandlers,
|
||||
filter?: string,
|
||||
_localize?: LocalizeFunc
|
||||
): [HandlerObj[], HandlerObj[]] => {
|
||||
const integrations: HandlerObj[] = h.integrations.map((handler) => ({
|
||||
name: domainToName(this.hass.localize, handler),
|
||||
slug: handler,
|
||||
}));
|
||||
@@ -70,17 +76,31 @@ class StepFlowPickHandler extends LitElement {
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.2,
|
||||
};
|
||||
const fuse = new Fuse(handlers, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
const helpers: HandlerObj[] = h.helpers.map((handler) => ({
|
||||
name: domainToName(this.hass.localize, handler),
|
||||
slug: handler,
|
||||
is_helper: true,
|
||||
}));
|
||||
return [
|
||||
new Fuse(integrations, options)
|
||||
.search(filter)
|
||||
.map((result) => result.item),
|
||||
new Fuse(helpers, options)
|
||||
.search(filter)
|
||||
.map((result) => result.item),
|
||||
];
|
||||
}
|
||||
return handlers.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
);
|
||||
return [
|
||||
integrations.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
),
|
||||
[],
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const handlers = this._getHandlers();
|
||||
const [integrations, helpers] = this._getHandlers();
|
||||
|
||||
const addDeviceRows: HandlerObj[] = ["zha", "zwave_js"]
|
||||
.filter((domain) => isComponentLoaded(this.hass, domain))
|
||||
@@ -115,8 +135,8 @@ class StepFlowPickHandler extends LitElement {
|
||||
<li divider padded class="divider" role="separator"></li>
|
||||
`
|
||||
: ""}
|
||||
${handlers.length
|
||||
? handlers.map((handler) => this._renderRow(handler))
|
||||
${integrations.length
|
||||
? integrations.map((handler) => this._renderRow(handler))
|
||||
: html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
@@ -139,6 +159,12 @@ class StepFlowPickHandler extends LitElement {
|
||||
>.
|
||||
</p>
|
||||
`}
|
||||
${helpers.length
|
||||
? html`
|
||||
<li divider padded class="divider" role="separator"></li>
|
||||
${helpers.map((handler) => this._renderRow(handler))}
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
@@ -162,7 +188,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
})}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span>${handler.name}</span>
|
||||
<span>${handler.name} ${handler.is_helper ? " (helper)" : ""}</span>
|
||||
${handler.is_add ? "" : html`<ha-icon-next slot="meta"></ha-icon-next>`}
|
||||
</mwc-list-item>
|
||||
`;
|
||||
@@ -236,6 +262,13 @@ class StepFlowPickHandler extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handler.is_helper) {
|
||||
navigate(`/config/helpers/add?domain=${handler.slug}`);
|
||||
// This closes dialog.
|
||||
fireEvent(this, "flow-update");
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: handler.slug,
|
||||
});
|
||||
@@ -250,7 +283,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
|
||||
if (handlers.length > 0) {
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: handlers[0].slug,
|
||||
handler: handlers[0][0].slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import { showMediaBrowserDialog } from "../../../components/media-player/show-me
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
computeMediaControls,
|
||||
handleMediaControlClick,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
@@ -47,7 +48,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj);
|
||||
const controls = computeMediaControls(stateObj, true);
|
||||
|
||||
return html`
|
||||
<div class="controls">
|
||||
@@ -202,6 +203,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
.basic-controls {
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -231,12 +233,10 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
this.hass!.callService(
|
||||
"media_player",
|
||||
(e.currentTarget! as HTMLElement).getAttribute("action")!,
|
||||
{
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
}
|
||||
handleMediaControlClick(
|
||||
this.hass!,
|
||||
this.stateObj!,
|
||||
(e.currentTarget as HTMLElement).getAttribute("action")!
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -46,7 +46,7 @@ class MoreInfoTimer extends LitElement {
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="remaining"
|
||||
extra-filters="remaining,restore"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,18 +1,23 @@
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-faded";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-markdown";
|
||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import {
|
||||
updateIsInstalling,
|
||||
UpdateEntity,
|
||||
updateIsInstalling,
|
||||
updateReleaseNotes,
|
||||
UPDATE_SUPPORT_BACKUP,
|
||||
UPDATE_SUPPORT_INSTALL,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
UPDATE_SUPPORT_RELEASE_NOTES,
|
||||
UPDATE_SUPPORT_SPECIFIC_VERSION,
|
||||
} from "../../../data/update";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -23,6 +28,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: UpdateEntity;
|
||||
|
||||
@state() private _releaseNotes?: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this.hass ||
|
||||
@@ -50,6 +59,9 @@ class MoreInfoUpdate extends LitElement {
|
||||
${this.stateObj.attributes.title
|
||||
? html`<h3>${this.stateObj.attributes.title}</h3>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<div class="row">
|
||||
<div class="key">
|
||||
@@ -89,11 +101,19 @@ class MoreInfoUpdate extends LitElement {
|
||||
</div>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.stateObj.attributes.release_summary
|
||||
${supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES) &&
|
||||
!this._error
|
||||
? this._releaseNotes === undefined
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<hr />
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._releaseNotes}></ha-markdown>
|
||||
</ha-faded> `
|
||||
: this.stateObj.attributes.release_summary
|
||||
? html`<hr />
|
||||
<ha-markdown
|
||||
.content=${this.stateObj.attributes.release_summary}
|
||||
></ha-markdown> `
|
||||
></ha-markdown>`
|
||||
: ""}
|
||||
${supportsFeature(this.stateObj, UPDATE_SUPPORT_BACKUP)
|
||||
? html`<hr />
|
||||
@@ -136,6 +156,18 @@ class MoreInfoUpdate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (supportsFeature(this.stateObj!, UPDATE_SUPPORT_RELEASE_NOTES)) {
|
||||
updateReleaseNotes(this.hass, this.stateObj!.entity_id)
|
||||
.then((result) => {
|
||||
this._releaseNotes = result;
|
||||
})
|
||||
.catch((err) => {
|
||||
this._error = err.message;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean | null {
|
||||
if (!supportsFeature(this.stateObj!, UPDATE_SUPPORT_BACKUP)) {
|
||||
return null;
|
||||
@@ -201,6 +233,10 @@ class MoreInfoUpdate extends LitElement {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-circular-progress {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -132,6 +132,12 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
*/
|
||||
@property() public tabs!: PageNavigation[];
|
||||
|
||||
/**
|
||||
* Force hides the filter menu.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ type: Boolean }) public hideFilterMenu = false;
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
public clearSelection() {
|
||||
@@ -195,16 +201,24 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.mainPage=${this.mainPage}
|
||||
.supervisor=${this.supervisor}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
${this.narrow
|
||||
? html`<div class="filter-menu">
|
||||
${this.numHidden || this.activeFilters
|
||||
? html`<span class="badge">${this.numHidden || "!"}</span>`
|
||||
: ""}
|
||||
<slot name="filter-menu"></slot>
|
||||
</div>`
|
||||
: ""}<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
${!this.hideFilterMenu
|
||||
? html`
|
||||
<div slot="toolbar-icon">
|
||||
${this.narrow
|
||||
? html`
|
||||
<div class="filter-menu">
|
||||
${this.numHidden || this.activeFilters
|
||||
? html`<span class="badge"
|
||||
>${this.numHidden || "!"}</span
|
||||
>`
|
||||
: ""}
|
||||
<slot name="filter-menu"></slot>
|
||||
</div>
|
||||
`
|
||||
: ""}<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
|
@@ -326,12 +326,9 @@ export class HaBlueprintAutomationEditor extends LitElement {
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
--settings-row-content-width: 100%;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row ha-textfield,
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
224
src/panels/config/backup/ha-config-backup.ts
Normal file
224
src/panels/config/backup/ha-config-backup.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
BackupContent,
|
||||
BackupData,
|
||||
fetchBackupInfo,
|
||||
generateBackup,
|
||||
getBackupDownloadUrl,
|
||||
removeBackup,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
|
||||
@customElement("ha-config-backup")
|
||||
class HaConfigBackup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _backupData?: BackupData;
|
||||
|
||||
private _columns = memoize(
|
||||
(narrow, _language): DataTableColumnContainer => ({
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.backup.name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (entry: string, backup: BackupContent) =>
|
||||
html`${entry}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: this.hass.localize("ui.panel.config.backup.size"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||
},
|
||||
date: {
|
||||
title: this.hass.localize("ui.panel.config.backup.created"),
|
||||
width: "15%",
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: string) =>
|
||||
relativeTime(new Date(entry), this.hass.locale),
|
||||
},
|
||||
|
||||
actions: {
|
||||
title: "",
|
||||
width: "15%",
|
||||
template: (_: string, backup: BackupContent) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.items=${[
|
||||
// Download Button
|
||||
{
|
||||
path: mdiDownload,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.download_backup"
|
||||
),
|
||||
action: () => this._downloadBackup(backup),
|
||||
},
|
||||
// Delete button
|
||||
{
|
||||
path: mdiDelete,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.remove_backup"
|
||||
),
|
||||
action: () => this._removeBackup(backup),
|
||||
},
|
||||
]}
|
||||
style="color: var(--secondary-text-color)"
|
||||
>
|
||||
</ha-icon-overflow-menu>`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _getItems = memoize((backupItems: BackupContent[]) =>
|
||||
backupItems.map((backup) => ({
|
||||
name: backup.name,
|
||||
slug: backup.slug,
|
||||
date: backup.date,
|
||||
size: backup.size,
|
||||
path: backup.path,
|
||||
}))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._backupData === undefined) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.backup}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_bakcups")}
|
||||
>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${this._backupData.backing_up}
|
||||
.label=${this._backupData.backing_up
|
||||
? this.hass.localize("ui.panel.config.backup.creating_backup")
|
||||
: this.hass.localize("ui.panel.config.backup.create_backup")}
|
||||
extended
|
||||
@click=${this._generateBackup}
|
||||
>
|
||||
${this._backupData.backing_up
|
||||
? html`<ha-circular-progress
|
||||
slot="icon"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getBackups();
|
||||
}
|
||||
|
||||
private async _getBackups(): Promise<void> {
|
||||
this._backupData = await fetchBackupInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
getBackupDownloadUrl(backup.slug)
|
||||
);
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private async _generateBackup(): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.backup.create.title"),
|
||||
text: this.hass.localize("ui.panel.config.backup.create.description"),
|
||||
confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateBackup(this.hass)
|
||||
.then(() => this._getBackups())
|
||||
.catch((err) => showAlertDialog(this, { text: (err as Error).message }));
|
||||
|
||||
await this._getBackups();
|
||||
}
|
||||
|
||||
private async _removeBackup(backup: BackupContent): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.backup.remove.title"),
|
||||
text: this.hass.localize("ui.panel.config.backup.remove.description", {
|
||||
name: backup.name,
|
||||
}),
|
||||
confirmText: this.hass.localize("ui.panel.config.backup.remove.confirm"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeBackup(this.hass, backup.slug);
|
||||
await this._getBackups();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-fab[disabled] {
|
||||
--mdc-theme-secondary: var(--disabled-text-color) !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup": HaConfigBackup;
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-formfield/mwc-formfield";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
@@ -20,9 +19,20 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import {
|
||||
createConfigFlow,
|
||||
handleConfigFlowStep,
|
||||
} from "../../../data/config_flow";
|
||||
import { DataEntryFlowStepCreateEntry } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
@@ -31,9 +41,12 @@ import {
|
||||
import {
|
||||
EntityRegistryEntryUpdateParams,
|
||||
ExtEntityRegistryEntry,
|
||||
fetchEntityRegistry,
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -42,29 +55,49 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import {
|
||||
ConfigEntry,
|
||||
deleteConfigEntry,
|
||||
getConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showEntityEditorDialog } from "./show-dialog-entity-editor";
|
||||
|
||||
const OVERRIDE_DEVICE_CLASSES = {
|
||||
cover: [
|
||||
"awning",
|
||||
"blind",
|
||||
"curtain",
|
||||
"damper",
|
||||
"door",
|
||||
"garage",
|
||||
"gate",
|
||||
"shade",
|
||||
"shutter",
|
||||
"window",
|
||||
[
|
||||
"awning",
|
||||
"blind",
|
||||
"curtain",
|
||||
"damper",
|
||||
"door",
|
||||
"garage",
|
||||
"gate",
|
||||
"shade",
|
||||
"shutter",
|
||||
"window",
|
||||
],
|
||||
],
|
||||
binary_sensor: [
|
||||
["lock"], // Lock
|
||||
["window", "door", "garage_door", "opening"], // Door
|
||||
["battery", "battery_charging"], // Battery
|
||||
["cold", "gas", "heat"], // Climate
|
||||
["running", "motion", "moving", "occupancy", "presence", "vibration"], // Presence
|
||||
["power", "plug", "light"], // Power
|
||||
[
|
||||
"smoke",
|
||||
"safety",
|
||||
"sound",
|
||||
"problem",
|
||||
"tamper",
|
||||
"carbon_monoxide",
|
||||
"moisture",
|
||||
], // Alarm
|
||||
],
|
||||
binary_sensor: ["window", "door", "garage_door", "opening"],
|
||||
};
|
||||
|
||||
const OVERRIDE_SENSOR_UNITS = {
|
||||
temperature: ["°C", "°F", "K"],
|
||||
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
|
||||
};
|
||||
|
||||
const SWITCH_AS_DOMAINS = ["light", "lock", "cover", "fan", "siren"];
|
||||
|
||||
@customElement("entity-registry-settings")
|
||||
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -79,24 +112,30 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _deviceClass?: string;
|
||||
|
||||
@state() private _switchAs = "switch";
|
||||
|
||||
@state() private _areaId?: string | null;
|
||||
|
||||
@state() private _disabledBy!: string | null;
|
||||
|
||||
@state() private _hiddenBy!: string | null;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
@state() private _device?: DeviceRegistryEntry;
|
||||
|
||||
@state() private _helperConfigEntry?: ConfigEntry;
|
||||
|
||||
@state() private _unit_of_measurement?: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
private _origEntityId!: string;
|
||||
|
||||
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
|
||||
|
||||
private _deviceClassOptions?: string[][];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
@@ -125,23 +164,48 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("entry")) {
|
||||
this._error = undefined;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
? this._deviceLookup[this.entry.device_id]
|
||||
: undefined;
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!changedProperties.has("entry")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
this._name = this.entry.name || "";
|
||||
this._icon = this.entry.icon || "";
|
||||
this._deviceClass =
|
||||
this.entry.device_class || this.entry.original_device_class;
|
||||
this._origEntityId = this.entry.entity_id;
|
||||
this._areaId = this.entry.area_id;
|
||||
this._entityId = this.entry.entity_id;
|
||||
this._disabledBy = this.entry.disabled_by;
|
||||
this._hiddenBy = this.entry.hidden_by;
|
||||
this._device =
|
||||
this.entry.device_id && this._deviceLookup
|
||||
? this._deviceLookup[this.entry.device_id]
|
||||
: undefined;
|
||||
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
if (domain === "sensor") {
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
|
||||
}
|
||||
|
||||
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
|
||||
if (!deviceClasses) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._deviceClassOptions = [[], []];
|
||||
for (const deviceClass of deviceClasses) {
|
||||
if (deviceClass.includes(this.entry.original_device_class!)) {
|
||||
this._deviceClassOptions[0] = deviceClass;
|
||||
} else {
|
||||
this._deviceClassOptions[1].push(...deviceClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,24 +261,81 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
: undefined}
|
||||
.disabled=${this._submitting}
|
||||
></ha-icon-picker>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
|
||||
(domain === "cover" && this.entry.original_device_class === null)
|
||||
${this._deviceClassOptions
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
.value=${this._deviceClass}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._deviceClassChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._deviceClassOptions[0].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
<li divider role="separator"></li>
|
||||
${this._deviceClassOptions[1].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
${this._deviceClass &&
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
|
||||
stateObj.attributes.unit_of_measurement
|
||||
)
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.unit_of_measurement"
|
||||
)}
|
||||
.value=${stateObj.attributes.unit_of_measurement}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._unitChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${OVERRIDE_SENSOR_UNITS[this._deviceClass].map(
|
||||
(unit: string) => html`
|
||||
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: ""}
|
||||
${domain === "switch"
|
||||
? html`<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.device_class"
|
||||
)}
|
||||
.value=${this._deviceClass}
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._deviceClassChanged}
|
||||
@selected=${this._switchAsChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${OVERRIDE_DEVICE_CLASSES[domain].map(
|
||||
(deviceClass: string) => html`
|
||||
<mwc-list-item .value=${deviceClass}>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
|
||||
)}
|
||||
<mwc-list-item value="switch" selected>
|
||||
${domainToName(this.hass.localize, "switch")}</mwc-list-item
|
||||
>
|
||||
${SWITCH_AS_DOMAINS.map(
|
||||
(as_domain) => html`
|
||||
<mwc-list-item .value=${as_domain}>
|
||||
${domainToName(this.hass.localize, as_domain)}
|
||||
</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
@@ -264,7 +385,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
)}:
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this._disabledBy && this._disabledBy !== "user"
|
||||
${this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration"
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_cause",
|
||||
"cause",
|
||||
@@ -286,7 +409,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${!this._hiddenBy && !this._disabledBy}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
this._device?.disabled_by ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -301,7 +426,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${this._hiddenBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -316,7 +443,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
.checked=${this._disabledBy !== null}
|
||||
.disabled=${(this._hiddenBy && this._hiddenBy !== "user") ||
|
||||
Boolean(this._device?.disabled_by) ||
|
||||
(this._disabledBy && this._disabledBy !== "user")}
|
||||
(this._disabledBy &&
|
||||
this._disabledBy !== "user" &&
|
||||
this._disabledBy !== "integration")}
|
||||
@change=${this._viewStatusChanged}
|
||||
></ha-radio>
|
||||
</mwc-formfield>
|
||||
@@ -378,7 +507,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
class="warning"
|
||||
@click=${this._confirmDeleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
(!this._helperConfigEntry && !stateObj.attributes.restored)}
|
||||
(!this._helperConfigEntry && !stateObj?.attributes.restored)}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.entity_registry.editor.delete")}
|
||||
</mwc-button>
|
||||
@@ -412,6 +541,18 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._deviceClass = ev.target.value;
|
||||
}
|
||||
|
||||
private _unitChanged(ev): void {
|
||||
this._error = undefined;
|
||||
this._unit_of_measurement = ev.target.value;
|
||||
}
|
||||
|
||||
private _switchAsChanged(ev): void {
|
||||
if (ev.target.value === "") {
|
||||
return;
|
||||
}
|
||||
this._switchAs = ev.target.value;
|
||||
}
|
||||
|
||||
private _areaPicked(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
this._areaId = ev.detail.value;
|
||||
@@ -445,6 +586,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _updateEntry(): Promise<void> {
|
||||
this._submitting = true;
|
||||
|
||||
const parent = (this.getRootNode() as ShadowRoot).host as HTMLElement;
|
||||
|
||||
const params: Partial<EntityRegistryEntryUpdateParams> = {
|
||||
name: this._name.trim() || null,
|
||||
icon: this._icon.trim() || null,
|
||||
@@ -452,6 +596,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
device_class: this._deviceClass || null,
|
||||
new_entity_id: this._entityId.trim(),
|
||||
};
|
||||
|
||||
const stateObj: HassEntity | undefined =
|
||||
this.hass.states[this.entry.entity_id];
|
||||
const domain = computeDomain(this.entry.entity_id);
|
||||
|
||||
if (
|
||||
this.entry.disabled_by !== this._disabledBy &&
|
||||
(this._disabledBy === null || this._disabledBy === "user")
|
||||
@@ -464,6 +613,13 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
params.hidden_by = this._hiddenBy;
|
||||
}
|
||||
if (
|
||||
domain === "sensor" &&
|
||||
stateObj?.attributes?.unit_of_measurement !== this._unit_of_measurement
|
||||
) {
|
||||
params.options_domain = "sensor";
|
||||
params.options = { unit_of_measurement: this._unit_of_measurement };
|
||||
}
|
||||
try {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass!,
|
||||
@@ -492,6 +648,46 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
if (this._switchAs !== "switch") {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass!.localize(
|
||||
"ui.dialogs.entity_registry.editor.switch_as_x_confirm",
|
||||
"domain",
|
||||
this._switchAs
|
||||
),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const configFlow = await createConfigFlow(this.hass, "switch_as_x");
|
||||
const result = (await handleConfigFlowStep(
|
||||
this.hass,
|
||||
configFlow.flow_id,
|
||||
{
|
||||
entity_id: this._entityId.trim(),
|
||||
target_domain: this._switchAs,
|
||||
}
|
||||
)) as DataEntryFlowStepCreateEntry;
|
||||
if (!result.result?.entry_id) {
|
||||
return;
|
||||
}
|
||||
const unsub = await this.hass.connection.subscribeEvents(() => {
|
||||
unsub();
|
||||
fetchEntityRegistry(this.hass.connection).then((entityRegistry) => {
|
||||
const entity = entityRegistry.find(
|
||||
(reg) => reg.config_entry_id === result.result!.entry_id
|
||||
);
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
showEntityEditorDialog(parent, {
|
||||
entity_id: entity.entity_id,
|
||||
});
|
||||
});
|
||||
}, "entity_registry_updated");
|
||||
}
|
||||
}
|
||||
|
||||
private async _confirmDeleteEntry(): Promise<void> {
|
||||
@@ -577,6 +773,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
margin: 8px 0;
|
||||
width: 340px;
|
||||
}
|
||||
li[divider] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -29,9 +29,9 @@ import type {
|
||||
SelectionChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
@@ -507,6 +507,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.data=${filteredEntities}
|
||||
.activeFilters=${activeFilters}
|
||||
.numHidden=${this._numHiddenEntities}
|
||||
.hideFilterMenu=${this._selectedEntities.length > 0}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.search"
|
||||
)}
|
||||
@@ -526,149 +527,155 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.hasFab=${includeZHAFab}
|
||||
>
|
||||
${this._selectedEntities.length
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.selected",
|
||||
"number",
|
||||
this._selectedEntities.length
|
||||
)}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-button @click=${this._enableSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._disableSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._hideSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._removeSelected} class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
id="enable-btn"
|
||||
@click=${this._enableSelected}
|
||||
.path=${mdiUndo}
|
||||
.label=${this.hass.localize("ui.common.enable")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="enable-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="disable-btn"
|
||||
@click=${this._disableSelected}
|
||||
.path=${mdiCancel}
|
||||
.label=${this.hass.localize("ui.common.disable")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="disable-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="hide-btn"
|
||||
@click=${this._hideSelected}
|
||||
.path=${mdiCancel}
|
||||
.label=${this.hass.localize("ui.common.hide")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="hide-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@click=${this._removeSelected}
|
||||
.path=${mdiDelete}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="remove-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`}
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.selected",
|
||||
"number",
|
||||
this._selectedEntities.length
|
||||
)}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-button @click=${this._enableSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._disableSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._hideSelected}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button
|
||||
@click=${this._removeSelected}
|
||||
class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
id="enable-btn"
|
||||
@click=${this._enableSelected}
|
||||
.path=${mdiUndo}
|
||||
.label=${this.hass.localize("ui.common.enable")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="enable-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.enable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="disable-btn"
|
||||
@click=${this._disableSelected}
|
||||
.path=${mdiCancel}
|
||||
.label=${this.hass.localize("ui.common.disable")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="disable-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.disable_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
id="hide-btn"
|
||||
@click=${this._hideSelected}
|
||||
.path=${mdiEyeOff}
|
||||
.label=${this.hass.localize("ui.common.hide")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="hide-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.hide_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@click=${this._removeSelected}
|
||||
.path=${mdiDelete}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
></ha-icon-button>
|
||||
<paper-tooltip animation-delay="0" for="remove-btn">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
)}
|
||||
</paper-tooltip>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div> `
|
||||
: html`<ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.filter"
|
||||
)}
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-icon-button>
|
||||
${this.narrow && activeFilters?.length
|
||||
? html`<mwc-list-item @click=${this._clearFilter}
|
||||
>${this.hass.localize(
|
||||
"ui.components.data-table.filtering_by"
|
||||
)}
|
||||
${activeFilters.join(", ")}
|
||||
<span class="clear">Clear</span></mwc-list-item
|
||||
>`
|
||||
: ""}
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showDisabledChanged}
|
||||
.selected=${this._showDisabled}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_disabled"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showHiddenChanged}
|
||||
.selected=${this._showHidden}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_hidden"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showRestoredChanged}
|
||||
graphic="control"
|
||||
.selected=${this._showUnavailable}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_unavailable"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showReadOnlyChanged}
|
||||
graphic="control"
|
||||
.selected=${this._showReadOnly}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_readonly"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
</ha-button-menu>`}
|
||||
`
|
||||
: html`
|
||||
<ha-button-menu slot="filter-menu" corner="BOTTOM_START" multi>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.filter"
|
||||
)}
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-icon-button>
|
||||
${this.narrow && activeFilters?.length
|
||||
? html`<mwc-list-item @click=${this._clearFilter}
|
||||
>${this.hass.localize(
|
||||
"ui.components.data-table.filtering_by"
|
||||
)}
|
||||
${activeFilters.join(", ")}
|
||||
<span class="clear">Clear</span></mwc-list-item
|
||||
>`
|
||||
: ""}
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showDisabledChanged}
|
||||
.selected=${this._showDisabled}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_disabled"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showHiddenChanged}
|
||||
.selected=${this._showHidden}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_hidden"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showRestoredChanged}
|
||||
graphic="control"
|
||||
.selected=${this._showUnavailable}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_unavailable"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
<ha-check-list-item
|
||||
@request-selected=${this._showReadOnlyChanged}
|
||||
graphic="control"
|
||||
.selected=${this._showReadOnly}
|
||||
left
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.entities.picker.filter.show_readonly"
|
||||
)}
|
||||
</ha-check-list-item>
|
||||
</ha-button-menu>
|
||||
`}
|
||||
${includeZHAFab
|
||||
? html`<a href="/config/zha/add" slot="fab">
|
||||
<ha-fab
|
||||
@@ -974,6 +981,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
}
|
||||
.header-btns {
|
||||
display: flex;
|
||||
}
|
||||
.header-btns > mwc-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiBackupRestore,
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiCellphoneCog,
|
||||
mdiCog,
|
||||
@@ -63,6 +64,13 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
iconColor: "#64B5F6",
|
||||
component: "blueprint",
|
||||
},
|
||||
{
|
||||
path: "/config/backup",
|
||||
translationKey: "backup",
|
||||
iconPath: mdiBackupRestore,
|
||||
iconColor: "#4084CD",
|
||||
component: "backup",
|
||||
},
|
||||
{
|
||||
path: "/hassio",
|
||||
translationKey: "supervisor",
|
||||
@@ -105,6 +113,15 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
||||
core: true,
|
||||
},
|
||||
],
|
||||
backup: [
|
||||
{
|
||||
path: "/config/backup",
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
iconPath: mdiBackupRestore,
|
||||
iconColor: "#4084CD",
|
||||
component: "backup",
|
||||
},
|
||||
],
|
||||
devices: [
|
||||
{
|
||||
component: "integrations",
|
||||
@@ -287,6 +304,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-automation",
|
||||
load: () => import("./automation/ha-config-automation"),
|
||||
},
|
||||
backup: {
|
||||
tag: "ha-config-backup",
|
||||
load: () => import("./backup/ha-config-backup"),
|
||||
},
|
||||
blueprint: {
|
||||
tag: "ha-config-blueprint",
|
||||
load: () => import("./blueprint/ha-config-blueprint"),
|
||||
|
@@ -6,7 +6,6 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
@@ -32,6 +31,7 @@ import "./forms/ha-input_text-form";
|
||||
import "./forms/ha-timer-form";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
|
||||
const HELPERS = {
|
||||
input_boolean: createInputBoolean,
|
||||
@@ -66,7 +66,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._domain = undefined;
|
||||
this._domain = params.domain;
|
||||
this._item = undefined;
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
@@ -146,10 +146,17 @@ export class DialogHelperDetail extends LitElement {
|
||||
dialogInitialFocus
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
<img
|
||||
slot="graphic"
|
||||
.path=${domainIcon(domain)}
|
||||
></ha-svg-icon>
|
||||
loading="lazy"
|
||||
src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<span class="item-text"> ${label} </span>
|
||||
</mwc-list-item>
|
||||
${!isLoaded
|
||||
|
@@ -21,16 +21,20 @@ class HaTimerForm extends LitElement {
|
||||
|
||||
@state() private _duration!: string | number | DurationDict;
|
||||
|
||||
@state() private _restore!: boolean;
|
||||
|
||||
set item(item: Timer) {
|
||||
this._item = item;
|
||||
if (item) {
|
||||
this._name = item.name || "";
|
||||
this._icon = item.icon || "";
|
||||
this._duration = item.duration || "00:00:00";
|
||||
this._restore = item.restore || false;
|
||||
} else {
|
||||
this._name = "";
|
||||
this._icon = "";
|
||||
this._duration = "00:00:00";
|
||||
this._restore = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +83,18 @@ class HaTimerForm extends LitElement {
|
||||
"ui.dialogs.helper_settings.timer.duration"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.helper_settings.timer.restore"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.configValue=${"restore"}
|
||||
.checked=${this._restore}
|
||||
@click=${this._toggleRestore}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -104,6 +120,13 @@ class HaTimerForm extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleRestore() {
|
||||
this._restore = !this._restore;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this._item, restore: this._restore },
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -29,6 +29,14 @@ import { showEntityEditorDialog } from "../entities/show-dialog-entity-editor";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { HELPER_DOMAINS } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
// This groups items by a key but only returns last entry per key.
|
||||
const groupByOne = <T>(
|
||||
@@ -219,6 +227,67 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getConfigEntries();
|
||||
if (this.route.path === "/add") {
|
||||
this._handleAdd();
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAdd() {
|
||||
const domain = extractSearchParam("domain");
|
||||
navigate("/config/helpers", { replace: true });
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
if (HELPER_DOMAINS.includes(domain)) {
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const handlers = await getConfigFlowHandlers(this.hass, "helper");
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
const integrations = await getConfigFlowHandlers(
|
||||
this.hass,
|
||||
"integration"
|
||||
);
|
||||
if (integrations.includes(domain)) {
|
||||
navigate(`/config/integrations/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.no_config_flow"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"title",
|
||||
domain,
|
||||
true
|
||||
);
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.integrations.confirm_new", {
|
||||
integration: domainToName(localize, domain),
|
||||
}),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
showConfigFlowDialog(this, {
|
||||
dialogClosedCallback: () => {
|
||||
this._getConfigEntries();
|
||||
},
|
||||
startFlowHandler: domain,
|
||||
showAdvanced: this.hass.userData?.showAdvanced,
|
||||
});
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
|
@@ -4,8 +4,9 @@ import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dia
|
||||
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
|
||||
|
||||
export interface ShowDialogHelperDetailParams {
|
||||
domain?: string;
|
||||
// Only used for config entries
|
||||
dialogClosedCallback: DataEntryFlowDialogParams["dialogClosedCallback"];
|
||||
dialogClosedCallback?: DataEntryFlowDialogParams["dialogClosedCallback"];
|
||||
}
|
||||
|
||||
export const showHelperDetailDialog = (
|
||||
|
@@ -67,6 +67,7 @@ import "./ha-ignored-config-entry-card";
|
||||
import "./ha-integration-card";
|
||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
|
||||
import { HELPER_DOMAINS } from "../helpers/const";
|
||||
|
||||
export interface ConfigEntryUpdatedEvent {
|
||||
entry: ConfigEntry;
|
||||
@@ -661,6 +662,19 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
const handlers = await getConfigFlowHandlers(this.hass, "integration");
|
||||
|
||||
if (!handlers.includes(domain)) {
|
||||
if (HELPER_DOMAINS.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const helpers = await getConfigFlowHandlers(this.hass, "helper");
|
||||
if (helpers.includes(domain)) {
|
||||
navigate(`/config/helpers/add?domain=${domain}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
|
@@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
@@ -24,9 +23,9 @@ import {
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed";
|
||||
import { showFixStatisticsUnsupportedUnitMetadataDialog } from "./show-dialog-statistics-fix-unsupported-unit-meta";
|
||||
import { showStatisticsAdjustSumDialog } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
const FIX_ISSUES_ORDER = {
|
||||
no_state: 0,
|
||||
@@ -116,27 +115,21 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
type: "overflow-menu",
|
||||
template: (
|
||||
_info,
|
||||
statistic: StatisticsMetaData
|
||||
) => html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.items=${[
|
||||
{
|
||||
path: mdiSlopeUphill,
|
||||
label: localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.adjust_sum"
|
||||
),
|
||||
action: () =>
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: statistic,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style="color: var(--secondary-text-color)"
|
||||
></ha-icon-overflow-menu>`,
|
||||
label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"),
|
||||
type: "icon-button",
|
||||
template: (_info, statistic: StatisticsMetaData) =>
|
||||
statistic.has_sum
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.adjust_sum"
|
||||
)}
|
||||
.path=${mdiSlopeUphill}
|
||||
.statistic=${statistic}
|
||||
@click=${this._showStatisticsAdjustSumDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: "",
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -154,6 +147,13 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _showStatisticsAdjustSumDialog(ev) {
|
||||
ev.stopPropagation();
|
||||
showStatisticsAdjustSumDialog(this, {
|
||||
statistic: ev.currentTarget.statistic,
|
||||
});
|
||||
}
|
||||
|
||||
private _rowClicked(ev) {
|
||||
const id = ev.detail.id;
|
||||
if (id in this.hass.states) {
|
||||
@@ -212,6 +212,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
source: "",
|
||||
state: this.hass.states[statisticId],
|
||||
issues: issues[statisticId],
|
||||
has_mean: false,
|
||||
has_sum: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -1,24 +1,37 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { LitElement, TemplateResult, html, CSSResultGroup } from "lit";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiChevronRight } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../components/ha-dialog";
|
||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-selector/ha-selector-datetime";
|
||||
import "../../../components/ha-selector/ha-selector-number";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
adjustStatisticsSum,
|
||||
fetchStatistics,
|
||||
StatisticValue,
|
||||
} from "../../../data/history";
|
||||
import type { DateTimeSelector, NumberSelector } from "../../../data/selector";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
|
||||
import type {
|
||||
HaFormBaseSchema,
|
||||
HaFormSchema,
|
||||
} from "../../../components/ha-form/types";
|
||||
import { adjustStatisticsSum } from "../../../data/history";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum";
|
||||
|
||||
let lastMoment: string | undefined;
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
|
||||
@customElement("dialog-statistics-adjust-sum")
|
||||
export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
@@ -26,29 +39,54 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
|
||||
@state() private _params?: DialogStatisticsAdjustSumParams;
|
||||
|
||||
@state() private _data?: {
|
||||
moment: string;
|
||||
amount: number;
|
||||
@state() private _busy = false;
|
||||
|
||||
@state() private _moment?: string;
|
||||
|
||||
@state() private _stats5min?: StatisticValue[];
|
||||
|
||||
@state() private _statsHour?: StatisticValue[];
|
||||
|
||||
@state() private _chosenStat?: StatisticValue;
|
||||
|
||||
private _origAmount?: number;
|
||||
|
||||
@state() private _amount?: number;
|
||||
|
||||
private _dateTimeSelector: DateTimeSelector = {
|
||||
datetime: {},
|
||||
};
|
||||
|
||||
@state() private _busy = false;
|
||||
private _amountSelector = memoizeOne(
|
||||
(unit_of_measurement: string): NumberSelector => ({
|
||||
number: {
|
||||
step: 0.01,
|
||||
unit_of_measurement,
|
||||
mode: "box",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
public showDialog(params: DialogStatisticsAdjustSumParams): void {
|
||||
this._params = params;
|
||||
this._busy = false;
|
||||
const now = new Date();
|
||||
this._data = {
|
||||
moment:
|
||||
lastMoment ||
|
||||
`${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`,
|
||||
amount: 0,
|
||||
};
|
||||
this._moment = `${now.getFullYear()}-${
|
||||
now.getMonth() + 1
|
||||
}-${now.getDate()} ${now.getHours()}:${
|
||||
now.getMinutes() - (now.getMinutes() % 5)
|
||||
}:00`;
|
||||
this._fetchStats();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._moment = undefined;
|
||||
this._stats5min = undefined;
|
||||
this._statsHour = undefined;
|
||||
this._origAmount = undefined;
|
||||
this._amount = undefined;
|
||||
this._chosenStat = undefined;
|
||||
this._busy = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -57,78 +95,201 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
let content: TemplateResult;
|
||||
|
||||
if (!this._chosenStat) {
|
||||
content = this._renderPickStatistic();
|
||||
} else {
|
||||
content = this._renderAdjustStat();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
@closed=${this.closeDialog}
|
||||
heading="Adjust sum for a specific time."
|
||||
heading="Adjust a statistic"
|
||||
>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._getSchema(this._params.statistic)}
|
||||
.data=${this._data}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.disabled=${this._busy}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._fixIssue}
|
||||
dialogInitialFocus
|
||||
label="Adjust"
|
||||
></mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
></mwc-button>
|
||||
${content}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getSchema = memoizeOne((statistic): HaFormSchema[] => [
|
||||
{
|
||||
type: "constant",
|
||||
name: "name",
|
||||
value: statistic.name || statistic.statistic_id,
|
||||
},
|
||||
{
|
||||
name: "moment",
|
||||
required: true,
|
||||
selector: {
|
||||
datetime: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "amount",
|
||||
required: true,
|
||||
default: 0,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: 0.1,
|
||||
unit_of_measurement: statistic.unit_of_measurement,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
private _computeLabel(value: HaFormBaseSchema) {
|
||||
switch (value.name) {
|
||||
case "name":
|
||||
return "Statistic";
|
||||
case "moment":
|
||||
return "Moment to adjust";
|
||||
case "amount":
|
||||
return "Amount";
|
||||
default:
|
||||
return value.name;
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.size !== 1 || !changedProps.has("hass")) {
|
||||
return true;
|
||||
}
|
||||
// We only respond to hass changes if the translations changed
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
return !oldHass || oldHass.localize !== this.hass.localize;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this._data = ev.detail.value;
|
||||
private _renderPickStatistic() {
|
||||
let stats: TemplateResult;
|
||||
|
||||
if (!this._stats5min || !this._statsHour) {
|
||||
stats = html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
} else if (this._statsHour.length < 2 && this._stats5min.length < 2) {
|
||||
stats = html`<p>No statistics found for this period.</p>`;
|
||||
} else {
|
||||
const data =
|
||||
this._stats5min.length >= 2 ? this._stats5min : this._statsHour;
|
||||
const unit = this._params!.statistic.unit_of_measurement;
|
||||
const rows: TemplateResult[] = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const stat = data[i];
|
||||
const growth = Math.round((stat.sum! - data[i - 1].sum!) * 100) / 100;
|
||||
rows.push(html`
|
||||
<mwc-list-item
|
||||
twoline
|
||||
hasMeta
|
||||
@click=${() => {
|
||||
this._chosenStat = stat;
|
||||
this._origAmount = growth;
|
||||
this._amount = growth;
|
||||
}}
|
||||
>
|
||||
<span>${growth} ${unit}</span>
|
||||
<span slot="secondary">
|
||||
${formatDateTime(new Date(stat.start), this.hass.locale)}
|
||||
</span>
|
||||
<ha-svg-icon slot="meta" .path=${mdiChevronRight}></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
`);
|
||||
}
|
||||
stats = html`${rows}`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="text-content">
|
||||
Sometimes the statistics end up being incorrect for a specific point in
|
||||
time. This can mess up your beautiful graphs! Select a time below to
|
||||
find the bad moment and adjust the data.
|
||||
</div>
|
||||
<ha-selector-datetime
|
||||
label="Pick a time"
|
||||
.hass=${this.hass}
|
||||
.selector=${this._dateTimeSelector}
|
||||
.value=${this._moment}
|
||||
@value-changed=${this._dateTimeSelectorChanged}
|
||||
></ha-selector-datetime>
|
||||
<div class="stat-list">${stats}</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
></mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dateTimeSelectorChanged(ev) {
|
||||
this._moment = ev.detail.value;
|
||||
this._fetchStats();
|
||||
}
|
||||
|
||||
private _renderAdjustStat() {
|
||||
return html`
|
||||
<div class="text-content">
|
||||
${this._params!.statistic.name || this._params!.statistic.statistic_id}
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<span>Start</span>
|
||||
<span
|
||||
>${formatDateTime(
|
||||
new Date(this._chosenStat!.start),
|
||||
this.hass.locale
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="table-row">
|
||||
<span>End</span>
|
||||
<span
|
||||
>${formatDateTime(
|
||||
new Date(this._chosenStat!.end),
|
||||
this.hass.locale
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-selector-number
|
||||
label="New Value"
|
||||
.hass=${this.hass}
|
||||
.selector=${this._amountSelector(
|
||||
this._params!.statistic.unit_of_measurement
|
||||
)}
|
||||
.value=${this._amount}
|
||||
.disabled=${this._busy}
|
||||
@value-changed=${(ev) => {
|
||||
this._amount = ev.detail.value;
|
||||
}}
|
||||
></ha-selector-number>
|
||||
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
label="Adjust"
|
||||
.disabled=${this._busy}
|
||||
@click=${() => {
|
||||
this._fixIssue();
|
||||
}}
|
||||
></mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
.label=${this.hass.localize("ui.common.back")}
|
||||
.disabled=${this._busy}
|
||||
@click=${() => {
|
||||
this._chosenStat = undefined;
|
||||
}}
|
||||
></mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchStats(): Promise<void> {
|
||||
this._stats5min = undefined;
|
||||
this._statsHour = undefined;
|
||||
const statId = this._params!.statistic.statistic_id;
|
||||
const moment = new Date(this._moment!);
|
||||
|
||||
// Search 3 hours before and 3 hours after chosen time
|
||||
const hourStatStart = new Date(moment.getTime());
|
||||
hourStatStart.setTime(hourStatStart.getTime() - 3 * 3600 * 1000);
|
||||
const hourStatEnd = new Date(moment.getTime());
|
||||
hourStatEnd.setTime(hourStatEnd.getTime() + 3 * 3600 * 1000);
|
||||
|
||||
const statsHourData = await fetchStatistics(
|
||||
this.hass,
|
||||
hourStatStart,
|
||||
hourStatEnd,
|
||||
[statId],
|
||||
"hour"
|
||||
);
|
||||
this._statsHour =
|
||||
statId in statsHourData ? statsHourData[statId].slice(0, 6) : [];
|
||||
|
||||
// Can't have 5 min data if no hourly data
|
||||
if (this._statsHour.length === 0) {
|
||||
this._stats5min = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Search 15 minutes before and 15 minutes after chosen time
|
||||
const minStatStart = new Date(moment.getTime());
|
||||
minStatStart.setTime(minStatStart.getTime() - 15 * 60 * 1000);
|
||||
const minStatEnd = new Date(moment.getTime());
|
||||
minStatEnd.setTime(minStatEnd.getTime() + 15 * 60 * 1000);
|
||||
|
||||
const stats5MinData = await fetchStatistics(
|
||||
this.hass,
|
||||
minStatStart,
|
||||
minStatEnd,
|
||||
[statId],
|
||||
"5minute"
|
||||
);
|
||||
|
||||
this._stats5min =
|
||||
statId in stats5MinData ? stats5MinData[statId].slice(0, 6) : [];
|
||||
}
|
||||
|
||||
private async _fixIssue(): Promise<void> {
|
||||
@@ -137,8 +298,8 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
await adjustStatisticsSum(
|
||||
this.hass,
|
||||
this._params!.statistic.statistic_id,
|
||||
this._data!.moment,
|
||||
this._data!.amount
|
||||
this._chosenStat!.start,
|
||||
this._amount! - this._origAmount!
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._busy = false;
|
||||
@@ -150,12 +311,59 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
|
||||
showToast(this, {
|
||||
message: "Statistic sum adjusted",
|
||||
});
|
||||
lastMoment = this._data!.moment;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyle, haStyleDialog];
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 850px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-height: 80%;
|
||||
--mdc-dialog-max-height: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-content,
|
||||
ha-selector-datetime,
|
||||
ha-selector-number {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
mwc-list-item {
|
||||
margin: 0 -24px;
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
.table-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-list {
|
||||
min-height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.stat-list ha-circular-progress {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ import {
|
||||
computeMediaControls,
|
||||
computeMediaDescription,
|
||||
getCurrentProgress,
|
||||
handleMediaControlClick,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
@@ -174,7 +175,7 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
UNAVAILABLE_STATES.includes(entityState) ||
|
||||
(entityState === "off" && !supportsFeature(stateObj, SUPPORT_TURN_ON));
|
||||
const hasNoImage = !this._image;
|
||||
const controls = computeMediaControls(stateObj);
|
||||
const controls = computeMediaControls(stateObj, false);
|
||||
const showControls =
|
||||
controls &&
|
||||
(!this._veryNarrow ||
|
||||
@@ -504,10 +505,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
|
||||
this.hass!.callService("media_player", action, {
|
||||
entity_id: this._config!.entity,
|
||||
});
|
||||
handleMediaControlClick(
|
||||
this.hass!,
|
||||
this._stateObj!,
|
||||
(e.currentTarget as HTMLElement).getAttribute("action")!
|
||||
);
|
||||
}
|
||||
|
||||
private _updateProgressBar(): void {
|
||||
|
@@ -116,7 +116,7 @@ export class HuiCardOptions extends LitElement {
|
||||
outline: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
:host:not(.panel) ::slotted(*) {
|
||||
:host(:not(.panel)) ::slotted(*) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,6 @@ export const configElementStyle = css`
|
||||
.suffix {
|
||||
margin: 0 8px;
|
||||
}
|
||||
hui-theme-select-editor,
|
||||
hui-action-editor,
|
||||
ha-select,
|
||||
ha-textfield,
|
||||
|
@@ -99,6 +99,14 @@ export class HuiAlarmPanelCardEditor
|
||||
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.name`);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alarm-panel.${
|
||||
schema.name === "states" ? "available_states" : schema.name
|
||||
|
@@ -69,6 +69,12 @@ export class HuiAreaCardEditor
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "area":
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.area.name");
|
||||
case "navigation_path":
|
||||
|
@@ -200,6 +200,14 @@ export class HuiButtonCardEditor
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -121,6 +121,14 @@ export class HuiCalendarCardEditor
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.title");
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.calendar.${schema.name}`
|
||||
);
|
||||
|
@@ -31,7 +31,7 @@ import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { EntitiesCardConfig } from "../../cards/types";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import { TIMESTAMP_RENDERING_FORMATS } from "../../components/types";
|
||||
import type { LovelaceRowConfig } from "../../entity-rows/types";
|
||||
import { headerFooterConfigStructs } from "../../header-footer/structs";
|
||||
@@ -265,12 +265,17 @@ export class HuiEntitiesCardEditor
|
||||
.configValue=${"title"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
<hui-theme-select-editor
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
></ha-theme-picker>
|
||||
<div class="side-by-side">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
|
@@ -112,6 +112,14 @@ export class HuiEntityCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
|
@@ -172,6 +172,12 @@ export class HuiGaugeCardEditor
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.gauge.needle_gauge"
|
||||
);
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
return (
|
||||
this.hass!.localize(
|
||||
|
@@ -117,11 +117,23 @@ export class HuiGlanceCardEditor
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: HaFormSchema) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.glance.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
|
||||
private _computeLabelCallback = (schema: HaFormSchema) => {
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.glance.${schema.name}`
|
||||
) ||
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user