mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 11:17:21 +00:00
Compare commits
176 Commits
websocket_
...
theme-by-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f0dfc62b | ||
|
|
d4125d433f | ||
|
|
c824e58e0a | ||
|
|
c6617718b7 | ||
|
|
cd1ca72e45 | ||
|
|
4f9ca3b173 | ||
|
|
1349c8520c | ||
|
|
6d1a55cc3a | ||
|
|
23a9ae6835 | ||
|
|
dbd1e928de | ||
|
|
e86ad21ce2 | ||
|
|
0d97afb3f2 | ||
|
|
03a415beff | ||
|
|
44cc75afbc | ||
|
|
748642a8d6 | ||
|
|
3d5c65d652 | ||
|
|
a26bf80b13 | ||
|
|
497c6c35f1 | ||
|
|
b0b06a2787 | ||
|
|
f3d55447ca | ||
|
|
1b3d4b77d3 | ||
|
|
6ec4041c4c | ||
|
|
d919e8d333 | ||
|
|
af7bb85667 | ||
|
|
9061e2039b | ||
|
|
906e6f4a88 | ||
|
|
73fbe9a69d | ||
|
|
2a0f69a629 | ||
|
|
9411a77f14 | ||
|
|
de3bf2e088 | ||
|
|
16181b48ae | ||
|
|
8682debe61 | ||
|
|
bdbc9bc1b4 | ||
|
|
79b9f8d083 | ||
|
|
3918194d2d | ||
|
|
e9fef1f873 | ||
|
|
35face602b | ||
|
|
803ac496f6 | ||
|
|
f1173dd84b | ||
|
|
44dcca9923 | ||
|
|
bd74d39dd8 | ||
|
|
172d6c3079 | ||
|
|
56539e8065 | ||
|
|
8f6867f142 | ||
|
|
d51f8995dd | ||
|
|
f2e35dc70a | ||
|
|
6487b9b7ea | ||
|
|
e50b658db7 | ||
|
|
6efe237639 | ||
|
|
4a94cfc05b | ||
|
|
7cbdb1dcfd | ||
|
|
553bb61db7 | ||
|
|
786ff787d1 | ||
|
|
28b3f2970a | ||
|
|
7d170a710e | ||
|
|
cc40b50675 | ||
|
|
b6eaff46e9 | ||
|
|
674bb0d16a | ||
|
|
6ff018afc9 | ||
|
|
ad48732bb7 | ||
|
|
fef162346a | ||
|
|
72d208d1ac | ||
|
|
5a8b1b0fd4 | ||
|
|
4cfc651799 | ||
|
|
b4a3f4cb2c | ||
|
|
f0507a88a6 | ||
|
|
fe041e442d | ||
|
|
e5fea98460 | ||
|
|
31180e3a9e | ||
|
|
ce0f02a45b | ||
|
|
53f090356e | ||
|
|
776c4da688 | ||
|
|
849922f7be | ||
|
|
a26701808f | ||
|
|
904ee2e418 | ||
|
|
11ae3a77e8 | ||
|
|
3a12019b64 | ||
|
|
6c2cf1ff60 | ||
|
|
02ae0b5864 | ||
|
|
85fe2213c1 | ||
|
|
7dbc78f1d6 | ||
|
|
f965a3504f | ||
|
|
077f5efe7e | ||
|
|
ef3bea71a0 | ||
|
|
fcf655b0ec | ||
|
|
b263b74916 | ||
|
|
0f4b6b423a | ||
|
|
72df585c5e | ||
|
|
4698a63642 | ||
|
|
6eb43a7d61 | ||
|
|
af35b15400 | ||
|
|
0d50d2664f | ||
|
|
ff1159402e | ||
|
|
f8742ae690 | ||
|
|
c786d26542 | ||
|
|
3f8ff94002 | ||
|
|
64a968543b | ||
|
|
aea98f702b | ||
|
|
863ff622be | ||
|
|
730cea6646 | ||
|
|
7d1f8d618a | ||
|
|
67b970fcaa | ||
|
|
38bcdaa6f6 | ||
|
|
8f1389de66 | ||
|
|
37ac796c8f | ||
|
|
716cd19d41 | ||
|
|
173725f011 | ||
|
|
ad561b885b | ||
|
|
d77bdf4ac6 | ||
|
|
ac3796ec31 | ||
|
|
8c3fdfb6fb | ||
|
|
b7c7d0b4b5 | ||
|
|
8b0e6eed3a | ||
|
|
603f884e8c | ||
|
|
97dfccf4c7 | ||
|
|
fd1e31c0cc | ||
|
|
1de740e7b5 | ||
|
|
5abfb90b16 | ||
|
|
6b691063a8 | ||
|
|
d1d746e7e6 | ||
|
|
2fcb64d4a1 | ||
|
|
3769f8c7c0 | ||
|
|
f0a56e75f5 | ||
|
|
15f33e1f19 | ||
|
|
181122177b | ||
|
|
684cd0f627 | ||
|
|
277202e363 | ||
|
|
b388d1fd42 | ||
|
|
251e6399f5 | ||
|
|
f44c5d7a63 | ||
|
|
cae1ca52f0 | ||
|
|
f8de2c64a5 | ||
|
|
34ef5be720 | ||
|
|
1402802031 | ||
|
|
816989ab4d | ||
|
|
d4497ca39c | ||
|
|
6e39242ca3 | ||
|
|
0197e32783 | ||
|
|
87dfed4beb | ||
|
|
dae991dc89 | ||
|
|
6197e3483b | ||
|
|
b2a6c8bd36 | ||
|
|
938855e13c | ||
|
|
a8712e3b8e | ||
|
|
b15b577057 | ||
|
|
653aeae3d8 | ||
|
|
0aea6141ad | ||
|
|
5243c1d871 | ||
|
|
9449f5ad0a | ||
|
|
c337bc5f97 | ||
|
|
6aab60cf45 | ||
|
|
52e9bc3213 | ||
|
|
e48b2383cf | ||
|
|
002a249777 | ||
|
|
10498ce18d | ||
|
|
6a5936b2b2 | ||
|
|
dc68aaa803 | ||
|
|
e7931ce049 | ||
|
|
59b2582fe3 | ||
|
|
8577b0721c | ||
|
|
91319be855 | ||
|
|
0dff538298 | ||
|
|
6ac6d9c6eb | ||
|
|
6ba0071296 | ||
|
|
fef5dc4232 | ||
|
|
ce58962dbb | ||
|
|
9fb1e1d2ed | ||
|
|
a29544c1e6 | ||
|
|
b2b71edd04 | ||
|
|
028472fc7b | ||
|
|
b056ce228b | ||
|
|
0cd4256c0e | ||
|
|
e274c5b23f | ||
|
|
ea57846465 | ||
|
|
3f2e2bc659 | ||
|
|
e3f2f66206 |
@@ -11,6 +11,9 @@
|
|||||||
"DEV_CONTAINER": "1",
|
"DEV_CONTAINER": "1",
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"remoteEnv": {
|
||||||
|
"NODE_OPTIONS": "--max_old_space_size=8192"
|
||||||
|
},
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
mockEntityRegistry(hass, [
|
mockEntityRegistry(hass, [
|
||||||
{
|
{
|
||||||
config_entry_id: "co2signal",
|
config_entry_id: "co2signal",
|
||||||
|
config_subentry_id: null,
|
||||||
device_id: "co2signal",
|
device_id: "co2signal",
|
||||||
area_id: null,
|
area_id: null,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
@@ -85,6 +86,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
config_entry_id: "co2signal",
|
config_entry_id: "co2signal",
|
||||||
|
config_subentry_id: null,
|
||||||
device_id: "co2signal",
|
device_id: "co2signal",
|
||||||
area_id: null,
|
area_id: null,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
|||||||
supports_remove_device: false,
|
supports_remove_device: false,
|
||||||
supports_unload: true,
|
supports_unload: true,
|
||||||
supports_reconfigure: true,
|
supports_reconfigure: true,
|
||||||
|
supported_subentry_types: {},
|
||||||
pref_disable_new_entities: false,
|
pref_disable_new_entities: false,
|
||||||
pref_disable_polling: false,
|
pref_disable_polling: false,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_1"],
|
config_entries: ["config_entry_1"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
@@ -71,6 +72,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_2"],
|
config_entries: ["config_entry_2"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
@@ -94,6 +96,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: null,
|
area_id: null,
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_3"],
|
config_entries: ["config_entry_3"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_1"],
|
config_entries: ["config_entry_1"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
@@ -70,6 +71,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_2"],
|
config_entries: ["config_entry_2"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
@@ -93,6 +95,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
area_id: null,
|
area_id: null,
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
config_entries: ["config_entry_3"],
|
config_entries: ["config_entry_3"],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ const createConfigEntry = (
|
|||||||
supports_remove_device: false,
|
supports_remove_device: false,
|
||||||
supports_unload: true,
|
supports_unload: true,
|
||||||
supports_reconfigure: true,
|
supports_reconfigure: true,
|
||||||
|
supported_subentry_types: {},
|
||||||
|
num_subentries: 0,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
pref_disable_new_entities: false,
|
pref_disable_new_entities: false,
|
||||||
pref_disable_polling: false,
|
pref_disable_polling: false,
|
||||||
@@ -188,6 +190,7 @@ const createEntityRegistryEntries = (
|
|||||||
): EntityRegistryEntry[] => [
|
): EntityRegistryEntry[] => [
|
||||||
{
|
{
|
||||||
config_entry_id: item.entry_id,
|
config_entry_id: item.entry_id,
|
||||||
|
config_subentry_id: null,
|
||||||
device_id: "mock-device-id",
|
device_id: "mock-device-id",
|
||||||
area_id: null,
|
area_id: null,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
@@ -214,6 +217,7 @@ const createDeviceRegistryEntries = (
|
|||||||
{
|
{
|
||||||
entry_type: null,
|
entry_type: null,
|
||||||
config_entries: [item.entry_id],
|
config_entries: [item.entry_id],
|
||||||
|
config_entries_subentries: {},
|
||||||
connections: [],
|
connections: [],
|
||||||
manufacturer: "ESPHome",
|
manufacturer: "ESPHome",
|
||||||
model: "Mock Device",
|
model: "Mock Device",
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { IFuseOptions } from "fuse.js";
|
import type { IFuseOptions } from "fuse.js";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
|
||||||
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
import type { StoreAddon } from "../../../src/data/supervisor/store";
|
||||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
|
||||||
|
|
||||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||||
const options: IFuseOptions<StoreAddon> = {
|
const options: IFuseOptions<StoreAddon> = {
|
||||||
@@ -10,8 +8,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
|||||||
isCaseSensitive: false,
|
isCaseSensitive: false,
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
minMatchCharLength: Math.min(filter.length, 2),
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
getFn: getStripDiacriticsFn,
|
ignoreDiacritics: true,
|
||||||
};
|
};
|
||||||
const fuse = new Fuse(addons, options);
|
const fuse = new Fuse(addons, options);
|
||||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
return fuse.search(filter).map((result) => result.item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"backup-uploaded": { backup: HassioBackup };
|
"hassio-backup-uploaded": { backup: HassioBackup };
|
||||||
"backup-cleared": undefined;
|
"backup-cleared": undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
|
|||||||
this._uploading = true;
|
this._uploading = true;
|
||||||
try {
|
try {
|
||||||
const backup = await uploadBackup(this.hass, file);
|
const backup = await uploadBackup(this.hass, file);
|
||||||
fireEvent(this, "backup-uploaded", { backup: backup.data });
|
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: "Upload failed",
|
title: "Upload failed",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { customElement, property, query } from "lit/decorators";
|
|||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { formatDate } from "../../../src/common/datetime/format_date";
|
import { formatDate } from "../../../src/common/datetime/format_date";
|
||||||
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
|
||||||
import "../../../src/components/ha-checkbox";
|
import "../../../src/components/ha-checkbox";
|
||||||
import "../../../src/components/ha-formfield";
|
import "../../../src/components/ha-formfield";
|
||||||
import "../../../src/components/ha-textfield";
|
import "../../../src/components/ha-textfield";
|
||||||
@@ -19,13 +18,10 @@ import type {
|
|||||||
} from "../../../src/data/hassio/backup";
|
} from "../../../src/data/hassio/backup";
|
||||||
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
|
||||||
import type { HomeAssistant, TranslationDict } from "../../../src/types";
|
import type { HomeAssistant } from "../../../src/types";
|
||||||
import "./supervisor-formfield-label";
|
import "./supervisor-formfield-label";
|
||||||
import type { HaTextField } from "../../../src/components/ha-textfield";
|
import type { HaTextField } from "../../../src/components/ha-textfield";
|
||||||
|
|
||||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
|
||||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
|
||||||
|
|
||||||
interface CheckboxItem {
|
interface CheckboxItem {
|
||||||
slug: string;
|
slug: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
@@ -67,8 +63,6 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
|
|||||||
export class SupervisorBackupContent extends LitElement {
|
export class SupervisorBackupContent extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public supervisor?: Supervisor;
|
@property({ attribute: false }) public supervisor?: Supervisor;
|
||||||
|
|
||||||
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
@property({ attribute: false }) public backup?: HassioBackupDetail;
|
||||||
@@ -115,10 +109,6 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
this._focusTarget?.focus();
|
this._focusTarget?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _localize = (key: BackupOrRestoreKey) =>
|
|
||||||
this.supervisor?.localize(`backup.${key}`) ||
|
|
||||||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.onboarding && !this.supervisor) {
|
if (!this.onboarding && !this.supervisor) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -132,8 +122,8 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${this.backup
|
${this.backup
|
||||||
? html`<div class="details">
|
? html`<div class="details">
|
||||||
${this.backup.type === "full"
|
${this.backup.type === "full"
|
||||||
? this._localize("full_backup")
|
? this.supervisor?.localize("backup.full_backup")
|
||||||
: this._localize("partial_backup")}
|
: this.supervisor?.localize("backup.partial_backup")}
|
||||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||||
${this.hass
|
${this.hass
|
||||||
? formatDateTime(
|
? formatDateTime(
|
||||||
@@ -145,7 +135,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
</div>`
|
</div>`
|
||||||
: html`<ha-textfield
|
: html`<ha-textfield
|
||||||
name="backupName"
|
name="backupName"
|
||||||
.label=${this._localize("name")}
|
.label=${this.supervisor?.localize("backup.name")}
|
||||||
.value=${this.backupName}
|
.value=${this.backupName}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
>
|
>
|
||||||
@@ -153,11 +143,13 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${!this.backup || this.backup.type === "full"
|
${!this.backup || this.backup.type === "full"
|
||||||
? html`<div class="sub-header">
|
? html`<div class="sub-header">
|
||||||
${!this.backup
|
${!this.backup
|
||||||
? this._localize("type")
|
? this.supervisor?.localize("backup.type")
|
||||||
: this._localize("select_type")}
|
: this.supervisor?.localize("backup.select_type")}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-types">
|
<div class="backup-types">
|
||||||
<ha-formfield .label=${this._localize("full_backup")}>
|
<ha-formfield
|
||||||
|
.label=${this.supervisor?.localize("backup.full_backup")}
|
||||||
|
>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="full"
|
value="full"
|
||||||
@@ -166,7 +158,9 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-radio>
|
</ha-radio>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<ha-formfield .label=${this._localize("partial_backup")}>
|
<ha-formfield
|
||||||
|
.label=${this.supervisor?.localize("backup.partial_backup")}
|
||||||
|
>
|
||||||
<ha-radio
|
<ha-radio
|
||||||
@change=${this._handleRadioValueChanged}
|
@change=${this._handleRadioValueChanged}
|
||||||
value="partial"
|
value="partial"
|
||||||
@@ -202,7 +196,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this._localize("folders")}
|
.label=${this.supervisor?.localize("backup.folders")}
|
||||||
.iconPath=${mdiFolder}
|
.iconPath=${mdiFolder}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@@ -222,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-formfield
|
<ha-formfield
|
||||||
.label=${html`<supervisor-formfield-label
|
.label=${html`<supervisor-formfield-label
|
||||||
.label=${this._localize("addons")}
|
.label=${this.supervisor?.localize("backup.addons")}
|
||||||
.iconPath=${mdiPuzzle}
|
.iconPath=${mdiPuzzle}
|
||||||
>
|
>
|
||||||
</supervisor-formfield-label>`}
|
</supervisor-formfield-label>`}
|
||||||
@@ -247,7 +241,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${!this.backup
|
${!this.backup
|
||||||
? html`<ha-formfield
|
? html`<ha-formfield
|
||||||
class="password"
|
class="password"
|
||||||
.label=${this._localize("password_protection")}
|
.label=${this.supervisor?.localize("backup.password_protection")}
|
||||||
>
|
>
|
||||||
<ha-checkbox
|
<ha-checkbox
|
||||||
.checked=${this.backupHasPassword}
|
.checked=${this.backupHasPassword}
|
||||||
@@ -259,7 +253,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
${this.backupHasPassword
|
${this.backupHasPassword
|
||||||
? html`
|
? html`
|
||||||
<ha-password-field
|
<ha-password-field
|
||||||
.label=${this._localize("password")}
|
.label=${this.supervisor?.localize("backup.password")}
|
||||||
name="backupPassword"
|
name="backupPassword"
|
||||||
.value=${this.backupPassword}
|
.value=${this.backupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
@@ -267,7 +261,7 @@ export class SupervisorBackupContent extends LitElement {
|
|||||||
</ha-password-field>
|
</ha-password-field>
|
||||||
${!this.backup
|
${!this.backup
|
||||||
? html`<ha-password-field
|
? html`<ha-password-field
|
||||||
.label=${this._localize("confirm_password")}
|
.label=${this.supervisor?.localize("backup.confirm_password")}
|
||||||
name="confirmBackupPassword"
|
name="confirmBackupPassword"
|
||||||
.value=${this.confirmBackupPassword}
|
.value=${this.confirmBackupPassword}
|
||||||
@change=${this._handleTextValueChanged}
|
@change=${this._handleTextValueChanged}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
|
|||||||
</ha-header-bar>
|
</ha-header-bar>
|
||||||
</div>
|
</div>
|
||||||
<hassio-upload-backup
|
<hassio-upload-backup
|
||||||
@backup-uploaded=${this._backupUploaded}
|
@hassio-backup-uploaded=${this._backupUploaded}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></hassio-upload-backup>
|
></hassio-upload-backup>
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import { fileDownload } from "../../../../src/util/file_download";
|
|||||||
import "../../components/supervisor-backup-content";
|
import "../../components/supervisor-backup-content";
|
||||||
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
|
||||||
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
|
||||||
import type { BackupOrRestoreKey } from "../../util/translations";
|
|
||||||
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
|
||||||
|
|
||||||
@customElement("dialog-hassio-backup")
|
@customElement("dialog-hassio-backup")
|
||||||
@@ -43,7 +42,7 @@ class HassioBackupDialog
|
|||||||
extends LitElement
|
extends LitElement
|
||||||
implements HassDialog<HassioBackupDialogParams>
|
implements HassDialog<HassioBackupDialogParams>
|
||||||
{
|
{
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@@ -62,9 +61,13 @@ class HassioBackupDialog
|
|||||||
this._dialogParams = dialogParams;
|
this._dialogParams = dialogParams;
|
||||||
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
|
||||||
if (!this._backup) {
|
if (!this._backup) {
|
||||||
this._error = this._localize("no_backup_found");
|
this._error = this._dialogParams.supervisor?.localize(
|
||||||
|
"backup.no_backup_found"
|
||||||
|
);
|
||||||
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
|
||||||
this._error = this._localize("restore_no_home_assistant");
|
this._error = this._dialogParams.supervisor?.localize(
|
||||||
|
"backup.restore_no_home_assistant"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this._restoringBackup = false;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
@@ -82,13 +85,6 @@ class HassioBackupDialog
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _localize(key: BackupOrRestoreKey) {
|
|
||||||
return (
|
|
||||||
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
|
|
||||||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._dialogParams || !this._backup) {
|
if (!this._dialogParams || !this._backup) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -102,7 +98,7 @@ class HassioBackupDialog
|
|||||||
<ha-dialog-header slot="headline">
|
<ha-dialog-header slot="headline">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this._localize("close")}
|
.label=${this._dialogParams.supervisor?.localize("backup.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
.disabled=${this._restoringBackup}
|
.disabled=${this._restoringBackup}
|
||||||
@@ -150,7 +146,6 @@ class HassioBackupDialog
|
|||||||
.supervisor=${this._dialogParams.supervisor}
|
.supervisor=${this._dialogParams.supervisor}
|
||||||
.backup=${this._backup}
|
.backup=${this._backup}
|
||||||
.onboarding=${this._dialogParams.onboarding || false}
|
.onboarding=${this._dialogParams.onboarding || false}
|
||||||
.localize=${this._dialogParams.localize}
|
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
>
|
>
|
||||||
</supervisor-backup-content>
|
</supervisor-backup-content>
|
||||||
@@ -161,7 +156,7 @@ class HassioBackupDialog
|
|||||||
.disabled=${this._restoringBackup || !!this._error}
|
.disabled=${this._restoringBackup || !!this._error}
|
||||||
@click=${this._restoreClicked}
|
@click=${this._restoreClicked}
|
||||||
>
|
>
|
||||||
${this._localize("restore")}
|
${this._dialogParams.supervisor?.localize("backup.restore")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
</ha-md-dialog>
|
</ha-md-dialog>
|
||||||
@@ -196,18 +191,22 @@ class HassioBackupDialog
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!(await showConfirmationDialog(this, {
|
!(await showConfirmationDialog(this, {
|
||||||
title: this._localize(
|
title: supervisor?.localize(
|
||||||
|
`backup.${
|
||||||
this._backup!.type === "full"
|
this._backup!.type === "full"
|
||||||
? "confirm_restore_full_backup_title"
|
? "confirm_restore_full_backup_title"
|
||||||
: "confirm_restore_partial_backup_title"
|
: "confirm_restore_partial_backup_title"
|
||||||
|
}`
|
||||||
),
|
),
|
||||||
text: this._localize(
|
text: supervisor?.localize(
|
||||||
|
`backup.${
|
||||||
this._backup!.type === "full"
|
this._backup!.type === "full"
|
||||||
? "confirm_restore_full_backup_text"
|
? "confirm_restore_full_backup_text"
|
||||||
: "confirm_restore_partial_backup_text"
|
: "confirm_restore_partial_backup_text"
|
||||||
|
}`
|
||||||
),
|
),
|
||||||
confirmText: this._localize("restore"),
|
confirmText: supervisor?.localize("backup.restore"),
|
||||||
dismissText: this._localize("cancel"),
|
dismissText: supervisor?.localize("backup.cancel"),
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
this._restoringBackup = false;
|
this._restoringBackup = false;
|
||||||
@@ -227,7 +226,8 @@ class HassioBackupDialog
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this._error =
|
this._error =
|
||||||
error?.body?.message || this._localize("restore_start_failed");
|
error?.body?.message ||
|
||||||
|
supervisor?.localize("backup.restore_start_failed");
|
||||||
} finally {
|
} finally {
|
||||||
this._restoringBackup = false;
|
this._restoringBackup = false;
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ class HassioBackupDialog
|
|||||||
title: supervisor.localize("backup.remote_download_title"),
|
title: supervisor.localize("backup.remote_download_title"),
|
||||||
text: supervisor.localize("backup.remote_download_text"),
|
text: supervisor.localize("backup.remote_download_text"),
|
||||||
confirmText: supervisor.localize("backup.download"),
|
confirmText: supervisor.localize("backup.download"),
|
||||||
dismissText: this._localize("cancel"),
|
dismissText: supervisor?.localize("backup.cancel"),
|
||||||
});
|
});
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
@@ -302,7 +302,7 @@ class HassioBackupDialog
|
|||||||
private get _computeName() {
|
private get _computeName() {
|
||||||
return this._backup
|
return this._backup
|
||||||
? this._backup.name || this._backup.slug
|
? this._backup.name || this._backup.slug
|
||||||
: this._localize("unnamed_backup");
|
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
|
||||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||||
|
|
||||||
export interface HassioBackupDialogParams {
|
export interface HassioBackupDialogParams {
|
||||||
@@ -8,7 +7,6 @@ export interface HassioBackupDialogParams {
|
|||||||
onRestoring?: () => void;
|
onRestoring?: () => void;
|
||||||
onboarding?: boolean;
|
onboarding?: boolean;
|
||||||
supervisor?: Supervisor;
|
supervisor?: Supervisor;
|
||||||
localize?: LocalizeFunc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showHassioBackupDialog = (
|
export const showHassioBackupDialog = (
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import type { TranslationDict } from "../../../src/types";
|
|
||||||
|
|
||||||
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
|
||||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
|
||||||
25
package.json
25
package.json
@@ -33,7 +33,7 @@
|
|||||||
"@codemirror/language": "6.10.8",
|
"@codemirror/language": "6.10.8",
|
||||||
"@codemirror/legacy-modes": "6.4.2",
|
"@codemirror/legacy-modes": "6.4.2",
|
||||||
"@codemirror/search": "6.5.8",
|
"@codemirror/search": "6.5.8",
|
||||||
"@codemirror/state": "6.5.1",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.36.2",
|
"@codemirror/view": "6.36.2",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.17.2",
|
"@formatjs/intl-datetimeformat": "6.17.2",
|
||||||
@@ -91,14 +91,14 @@
|
|||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.6.2",
|
"@vaadin/combo-box": "24.6.4",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.6.2",
|
"@vaadin/vaadin-themable-mixin": "24.6.4",
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"barcode-detector": "2.3.1",
|
"barcode-detector": "3.0.0",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.0",
|
||||||
"comlink": "4.4.2",
|
"comlink": "4.4.2",
|
||||||
"core-js": "3.40.0",
|
"core-js": "3.40.0",
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
"dialog-polyfill": "0.5.6",
|
"dialog-polyfill": "0.5.6",
|
||||||
"echarts": "5.6.0",
|
"echarts": "5.6.0",
|
||||||
"element-internals-polyfill": "1.3.13",
|
"element-internals-polyfill": "1.3.13",
|
||||||
"fuse.js": "7.0.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"tsparticles-engine": "2.12.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.12.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
"ua-parser-js": "2.0.0",
|
"ua-parser-js": "2.0.1",
|
||||||
"vis-data": "7.1.9",
|
"vis-data": "7.1.9",
|
||||||
"vis-network": "9.1.9",
|
"vis-network": "9.1.9",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
"@rspack/cli": "1.2.2",
|
"@rspack/cli": "1.2.2",
|
||||||
"@rspack/core": "1.2.2",
|
"@rspack/core": "1.2.2",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.20",
|
"@types/chromecast-caf-receiver": "6.0.21",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
@@ -183,14 +183,14 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||||
"@typescript-eslint/parser": "8.21.0",
|
"@typescript-eslint/parser": "8.23.0",
|
||||||
"@vitest/coverage-v8": "3.0.4",
|
"@vitest/coverage-v8": "3.0.5",
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.0",
|
"del": "8.0.0",
|
||||||
"eslint": "9.19.0",
|
"eslint": "9.20.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.0.1",
|
"eslint-config-prettier": "10.0.1",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -219,12 +219,13 @@
|
|||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.4.2",
|
||||||
"rspack-manifest-plugin": "5.0.3",
|
"rspack-manifest-plugin": "5.0.3",
|
||||||
|
"serve": "14.2.4",
|
||||||
"sinon": "19.0.2",
|
"sinon": "19.0.2",
|
||||||
"tar": "7.4.3",
|
"tar": "7.4.3",
|
||||||
"terser-webpack-plugin": "5.3.11",
|
"terser-webpack-plugin": "5.3.11",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vitest": "3.0.4",
|
"vitest": "3.0.5",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20250129.0"
|
version = "20250205.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ echo Core is used from ${coreUrl}
|
|||||||
HASS_URL="$coreUrl" ./script/develop &
|
HASS_URL="$coreUrl" ./script/develop &
|
||||||
|
|
||||||
# serve the frontend
|
# serve the frontend
|
||||||
yarn dlx serve -l $frontendPort ./hass_frontend -s &
|
./node_modules/.bin/serve -p $frontendPort --single --no-port-switching --config ../script/serve-config.json ./hass_frontend &
|
||||||
|
|
||||||
# keep the script running while serving
|
# keep the script running while serving
|
||||||
wait
|
wait
|
||||||
|
|||||||
3
script/serve-config.json
Normal file
3
script/serve-config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cleanUrls": false
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { theme2hex } from "./convert-color";
|
import { theme2hex } from "./convert-color";
|
||||||
|
|
||||||
export const COLORS = [
|
export const COLORS = [
|
||||||
@@ -74,3 +75,12 @@ export function getGraphColorByIndex(
|
|||||||
getColorByIndex(index);
|
getColorByIndex(index);
|
||||||
return theme2hex(themeColor);
|
return theme2hex(themeColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAllGraphColors = memoizeOne(
|
||||||
|
(style: CSSStyleDeclaration) =>
|
||||||
|
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
|
||||||
|
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
|
||||||
|
// this is not ideal, but we need to memoize the colors
|
||||||
|
newArgs[0].getPropertyValue("--graph-color-1") ===
|
||||||
|
lastArgs[0].getPropertyValue("--graph-color-1")
|
||||||
|
);
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ const formatDateTimeMem = memoizeOne(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const formatDateTimeWithBrowserDefaults = (dateObj: Date) =>
|
||||||
|
formatDateTimeWithBrowserDefaultsMem().format(dateObj);
|
||||||
|
|
||||||
|
const formatDateTimeWithBrowserDefaultsMem = memoizeOne(
|
||||||
|
() =>
|
||||||
|
new Intl.DateTimeFormat(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Aug 9, 2021, 8:23 AM
|
// Aug 9, 2021, 8:23 AM
|
||||||
export const formatShortDateTimeWithYear = (
|
export const formatShortDateTimeWithYear = (
|
||||||
dateObj: Date,
|
dateObj: Date,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
|
|||||||
indeterminate
|
indeterminate
|
||||||
></ha-circular-progress>
|
></ha-circular-progress>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
@@ -117,6 +117,9 @@ export class HaProgressButton extends LitElement {
|
|||||||
mwc-button.error slot {
|
mwc-button.error slot {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
:host([destructive]) {
|
||||||
|
--mdc-theme-primary: var(--error-color);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import type { XAXisOption } from "echarts/types/dist/shared";
|
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import {
|
import {
|
||||||
formatDateMonth,
|
formatDateMonth,
|
||||||
@@ -7,14 +6,18 @@ import {
|
|||||||
formatDateVeryShort,
|
formatDateVeryShort,
|
||||||
formatDateWeekdayShort,
|
formatDateWeekdayShort,
|
||||||
} from "../../common/datetime/format_date";
|
} from "../../common/datetime/format_date";
|
||||||
import { formatTime } from "../../common/datetime/format_time";
|
import {
|
||||||
|
formatTime,
|
||||||
|
formatTimeWithSeconds,
|
||||||
|
} from "../../common/datetime/format_time";
|
||||||
|
|
||||||
export function getLabelFormatter(
|
export function formatTimeLabel(
|
||||||
|
value: number | Date,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
config: HassConfig,
|
config: HassConfig,
|
||||||
dayDifference = 0
|
minutesDifference: number
|
||||||
) {
|
) {
|
||||||
return (value: number | Date) => {
|
const dayDifference = minutesDifference / 60 / 24;
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (dayDifference > 88) {
|
if (dayDifference > 88) {
|
||||||
return date.getMonth() === 0
|
return date.getMonth() === 0
|
||||||
@@ -33,30 +36,16 @@ export function getLabelFormatter(
|
|||||||
if (dayDifference > 2) {
|
if (dayDifference > 2) {
|
||||||
return formatDateWeekdayShort(date, locale, config);
|
return formatDateWeekdayShort(date, locale, config);
|
||||||
}
|
}
|
||||||
// show only date for the beginning of the day
|
if (minutesDifference && minutesDifference < 5) {
|
||||||
|
return formatTimeWithSeconds(date, locale, config);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
date.getHours() === 0 &&
|
date.getHours() === 0 &&
|
||||||
date.getMinutes() === 0 &&
|
date.getMinutes() === 0 &&
|
||||||
date.getSeconds() === 0
|
date.getSeconds() === 0
|
||||||
) {
|
) {
|
||||||
|
// show only date for the beginning of the day
|
||||||
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
return `{bold|${formatDateVeryShort(date, locale, config)}}`;
|
||||||
}
|
}
|
||||||
return formatTime(date, locale, config);
|
return formatTime(date, locale, config);
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimeAxisLabelConfig(
|
|
||||||
locale: FrontendLocaleData,
|
|
||||||
config: HassConfig,
|
|
||||||
dayDifference?: number
|
|
||||||
): XAXisOption["axisLabel"] {
|
|
||||||
return {
|
|
||||||
formatter: getLabelFormatter(locale, config, dayDifference),
|
|
||||||
rich: {
|
|
||||||
bold: {
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hideOverlap: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import { consume } from "@lit-labs/context";
|
||||||
import { css, html, nothing, LitElement } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import { mdiRestart } from "@mdi/js";
|
|
||||||
import type { EChartsType } from "echarts/core";
|
|
||||||
import type { DataZoomComponentOption } from "echarts/components";
|
|
||||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
|
import { mdiRestart } from "@mdi/js";
|
||||||
|
import { differenceInMinutes } from "date-fns";
|
||||||
|
import type { DataZoomComponentOption } from "echarts/components";
|
||||||
|
import type { EChartsType } from "echarts/core";
|
||||||
import type {
|
import type {
|
||||||
ECElementEvent,
|
ECElementEvent,
|
||||||
XAXisOption,
|
XAXisOption,
|
||||||
YAXisOption,
|
YAXisOption,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
import { consume } from "@lit-labs/context";
|
import type { PropertyValues } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { getAllGraphColors } from "../../common/color/colors";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||||
|
import { themesContext } from "../../data/context";
|
||||||
|
import type { Themes } from "../../data/ws-themes";
|
||||||
|
import type { ECOption } from "../../resources/echarts";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { isMac } from "../../util/is_mac";
|
import { isMac } from "../../util/is_mac";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import type { ECOption } from "../../resources/echarts";
|
import { formatTimeLabel } from "./axis-label";
|
||||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
|
||||||
import type { Themes } from "../../data/ws-themes";
|
|
||||||
import { themesContext } from "../../data/context";
|
|
||||||
|
|
||||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||||
|
|
||||||
@@ -44,6 +48,10 @@ export class HaChartBase extends LitElement {
|
|||||||
|
|
||||||
@state() private _isZoomed = false;
|
@state() private _isZoomed = false;
|
||||||
|
|
||||||
|
@state() private _zoomRatio = 1;
|
||||||
|
|
||||||
|
@state() private _minutesDifference = 24 * 60;
|
||||||
|
|
||||||
private _modifierPressed = false;
|
private _modifierPressed = false;
|
||||||
|
|
||||||
private _isTouchDevice = "ontouchstart" in window;
|
private _isTouchDevice = "ontouchstart" in window;
|
||||||
@@ -135,16 +143,7 @@ export class HaChartBase extends LitElement {
|
|||||||
this.chart.setOption(this._createOptions(), {
|
this.chart.setOption(this._createOptions(), {
|
||||||
lazyUpdate: true,
|
lazyUpdate: true,
|
||||||
// if we replace the whole object, it will reset the dataZoom
|
// if we replace the whole object, it will reset the dataZoom
|
||||||
replaceMerge: [
|
replaceMerge: ["grid"],
|
||||||
"xAxis",
|
|
||||||
"yAxis",
|
|
||||||
"dataZoom",
|
|
||||||
"dataset",
|
|
||||||
"tooltip",
|
|
||||||
"legend",
|
|
||||||
"grid",
|
|
||||||
"visualMap",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +151,10 @@ export class HaChartBase extends LitElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="chart-container"
|
class=${classMap({
|
||||||
|
"chart-container": true,
|
||||||
|
"has-legend": !!this.options?.legend,
|
||||||
|
})}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
height: this.height ?? `${this._getDefaultHeight()}px`,
|
height: this.height ?? `${this._getDefaultHeight()}px`,
|
||||||
})}
|
})}
|
||||||
@@ -173,6 +175,14 @@ export class HaChartBase extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _formatTimeLabel = (value: number | Date) =>
|
||||||
|
formatTimeLabel(
|
||||||
|
value,
|
||||||
|
this.hass.locale,
|
||||||
|
this.hass.config,
|
||||||
|
this._minutesDifference * this._zoomRatio
|
||||||
|
);
|
||||||
|
|
||||||
private async _setupChart() {
|
private async _setupChart() {
|
||||||
if (this._loading) return;
|
if (this._loading) return;
|
||||||
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
|
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
|
||||||
@@ -183,10 +193,9 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
const echarts = (await import("../../resources/echarts")).default;
|
const echarts = (await import("../../resources/echarts")).default;
|
||||||
|
|
||||||
this.chart = echarts.init(
|
echarts.registerTheme("custom", this._createTheme());
|
||||||
container,
|
|
||||||
this._themes.darkMode ? "dark" : "light"
|
this.chart = echarts.init(container, "custom");
|
||||||
);
|
|
||||||
this.chart.on("legendselectchanged", (params: any) => {
|
this.chart.on("legendselectchanged", (params: any) => {
|
||||||
if (this.externalHidden) {
|
if (this.externalHidden) {
|
||||||
const isSelected = params.selected[params.name];
|
const isSelected = params.selected[params.name];
|
||||||
@@ -200,6 +209,7 @@ export class HaChartBase extends LitElement {
|
|||||||
this.chart.on("datazoom", (e: any) => {
|
this.chart.on("datazoom", (e: any) => {
|
||||||
const { start, end } = e.batch?.[0] ?? e;
|
const { start, end } = e.batch?.[0] ?? e;
|
||||||
this._isZoomed = start !== 0 || end !== 100;
|
this._isZoomed = start !== 0 || end !== 100;
|
||||||
|
this._zoomRatio = (end - start) / 100;
|
||||||
});
|
});
|
||||||
this.chart.on("click", (e: ECElementEvent) => {
|
this.chart.on("click", (e: ECElementEvent) => {
|
||||||
fireEvent(this, "chart-click", e);
|
fireEvent(this, "chart-click", e);
|
||||||
@@ -237,24 +247,60 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions(): ECOption {
|
private _createOptions(): ECOption {
|
||||||
const darkMode = this._themes.darkMode ?? false;
|
let xAxis = this.options?.xAxis;
|
||||||
|
if (xAxis) {
|
||||||
|
xAxis = Array.isArray(xAxis) ? xAxis : [xAxis];
|
||||||
|
xAxis = xAxis.map((axis: XAXisOption) => {
|
||||||
|
if (axis.type !== "time" || axis.show === false) {
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
if (axis.max && axis.min) {
|
||||||
|
this._minutesDifference = differenceInMinutes(
|
||||||
|
axis.max as Date,
|
||||||
|
axis.min as Date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const dayDifference = this._minutesDifference / 60 / 24;
|
||||||
|
let minInterval: number | undefined;
|
||||||
|
if (dayDifference) {
|
||||||
|
minInterval =
|
||||||
|
dayDifference >= 89 // quarter
|
||||||
|
? 28 * 3600 * 24 * 1000
|
||||||
|
: dayDifference > 2
|
||||||
|
? 3600 * 24 * 1000
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
...axis,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: this._formatTimeLabel,
|
||||||
|
rich: {
|
||||||
|
bold: {
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hideOverlap: true,
|
||||||
|
...axis.axisLabel,
|
||||||
|
},
|
||||||
|
minInterval,
|
||||||
|
} as XAXisOption;
|
||||||
|
});
|
||||||
|
}
|
||||||
const options = {
|
const options = {
|
||||||
backgroundColor: "transparent",
|
|
||||||
animation: !this._reducedMotion,
|
animation: !this._reducedMotion,
|
||||||
darkMode,
|
darkMode: this._themes.darkMode ?? false,
|
||||||
aria: {
|
aria: {
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
dataZoom: this._getDataZoomConfig(),
|
dataZoom: this._getDataZoomConfig(),
|
||||||
...this.options,
|
...this.options,
|
||||||
legend: this.options?.legend
|
xAxis,
|
||||||
? {
|
|
||||||
// we should create our own theme but this is a quick fix for now
|
|
||||||
inactiveColor: darkMode ? "#444" : "#ccc",
|
|
||||||
...this.options.legend,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMobile = window.matchMedia(
|
const isMobile = window.matchMedia(
|
||||||
@@ -268,18 +314,207 @@ export class HaChartBase extends LitElement {
|
|||||||
tooltips.forEach((tooltip) => {
|
tooltips.forEach((tooltip) => {
|
||||||
tooltip.confine = true;
|
tooltip.confine = true;
|
||||||
tooltip.appendTo = undefined;
|
tooltip.appendTo = undefined;
|
||||||
|
tooltip.triggerOn = "click";
|
||||||
});
|
});
|
||||||
options.tooltip = tooltips;
|
options.tooltip = tooltips;
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createTheme() {
|
||||||
|
const style = getComputedStyle(this);
|
||||||
|
return {
|
||||||
|
color: getAllGraphColors(style),
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
textStyle: {
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
fontFamily: "Roboto, Noto, sans-serif",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
textStyle: {
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
subtextStyle: {
|
||||||
|
color: style.getPropertyValue("--secondary-text-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
lineStyle: {
|
||||||
|
width: 1.5,
|
||||||
|
},
|
||||||
|
symbolSize: 1,
|
||||||
|
symbol: "circle",
|
||||||
|
smooth: false,
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
itemStyle: {
|
||||||
|
barBorderWidth: 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
categoryAxis: {
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: false,
|
||||||
|
areaStyle: {
|
||||||
|
color: [
|
||||||
|
style.getPropertyValue("--divider-color") + "3F",
|
||||||
|
style.getPropertyValue("--divider-color") + "7F",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
valueAxis: {
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: false,
|
||||||
|
areaStyle: {
|
||||||
|
color: [
|
||||||
|
style.getPropertyValue("--divider-color") + "3F",
|
||||||
|
style.getPropertyValue("--divider-color") + "7F",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logAxis: {
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: false,
|
||||||
|
areaStyle: {
|
||||||
|
color: [
|
||||||
|
style.getPropertyValue("--divider-color") + "3F",
|
||||||
|
style.getPropertyValue("--divider-color") + "7F",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeAxis: {
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
show: true,
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: false,
|
||||||
|
areaStyle: {
|
||||||
|
color: [
|
||||||
|
style.getPropertyValue("--divider-color") + "3F",
|
||||||
|
style.getPropertyValue("--divider-color") + "7F",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
textStyle: {
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
},
|
||||||
|
inactiveColor: style.getPropertyValue("--disabled-text-color"),
|
||||||
|
pageIconColor: style.getPropertyValue("--primary-text-color"),
|
||||||
|
pageIconInactiveColor: style.getPropertyValue("--disabled-text-color"),
|
||||||
|
pageTextStyle: {
|
||||||
|
color: style.getPropertyValue("--secondary-text-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: style.getPropertyValue("--card-background-color"),
|
||||||
|
borderColor: style.getPropertyValue("--divider-color"),
|
||||||
|
textStyle: {
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
crossStyle: {
|
||||||
|
color: style.getPropertyValue("--divider-color"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeline: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private _getDefaultHeight() {
|
private _getDefaultHeight() {
|
||||||
return Math.max(this.clientWidth / 2, 400);
|
return Math.max(this.clientWidth / 2, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleZoomReset() {
|
private _handleZoomReset() {
|
||||||
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
|
||||||
|
this._modifierPressed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleWheel(e: WheelEvent) {
|
private _handleWheel(e: WheelEvent) {
|
||||||
@@ -302,10 +537,11 @@ export class HaChartBase extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
.chart-container {
|
.chart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: var(--chart-max-height, 400px);
|
max-height: var(--chart-max-height, 350px);
|
||||||
}
|
}
|
||||||
.chart {
|
.chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -321,6 +557,9 @@ export class HaChartBase extends LitElement {
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
}
|
}
|
||||||
|
.has-legend .zoom-reset {
|
||||||
|
top: 64px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { property, state } from "lit/decorators";
|
|||||||
import type { VisualMapComponentOption } from "echarts/components";
|
import type { VisualMapComponentOption } from "echarts/components";
|
||||||
import type { LineSeriesOption } from "echarts/charts";
|
import type { LineSeriesOption } from "echarts/charts";
|
||||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||||
import { differenceInDays } from "date-fns";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
@@ -18,10 +17,10 @@ import {
|
|||||||
getNumberFormatOptions,
|
getNumberFormatOptions,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
|
||||||
import { measureTextWidth } from "../../util/text";
|
import { measureTextWidth } from "../../util/text";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||||
|
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||||
|
|
||||||
const safeParseFloat = (value) => {
|
const safeParseFloat = (value) => {
|
||||||
const parsed = parseFloat(value);
|
const parsed = parseFloat(value);
|
||||||
@@ -72,6 +71,8 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
@state() private _chartOptions?: ECOption;
|
@state() private _chartOptions?: ECOption;
|
||||||
|
|
||||||
|
private _hiddenStats = new Set<string>();
|
||||||
|
|
||||||
@state() private _yWidth = 25;
|
@state() private _yWidth = 25;
|
||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
@@ -84,21 +85,73 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
.options=${this._chartOptions}
|
.options=${this._chartOptions}
|
||||||
.height=${this.height}
|
.height=${this.height}
|
||||||
style=${styleMap({ height: this.height })}
|
style=${styleMap({ height: this.height })}
|
||||||
|
external-hidden
|
||||||
|
@dataset-hidden=${this._datasetHidden}
|
||||||
|
@dataset-unhidden=${this._datasetUnhidden}
|
||||||
></ha-chart-base>
|
></ha-chart-base>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTooltip(params) {
|
private _renderTooltip(params: any) {
|
||||||
return params
|
const time = params[0].axisValue;
|
||||||
.map((param, index: number) => {
|
const title =
|
||||||
let value = `${formatNumber(
|
formatDateTimeWithSeconds(
|
||||||
param.value[1] as number,
|
new Date(time),
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
getNumberFormatOptions(
|
this.hass.config
|
||||||
undefined,
|
) + "<br>";
|
||||||
this.hass.entities[this._entityIds[param.seriesIndex]]
|
const datapoints: Record<string, any>[] = [];
|
||||||
|
this._chartData.forEach((dataset, index) => {
|
||||||
|
if (
|
||||||
|
dataset.tooltip?.show === false ||
|
||||||
|
this._hiddenStats.has(dataset.name as string)
|
||||||
)
|
)
|
||||||
)} ${this.unit}`;
|
return;
|
||||||
|
const param = params.find(
|
||||||
|
(p: Record<string, any>) => p.seriesIndex === index
|
||||||
|
);
|
||||||
|
if (param) {
|
||||||
|
datapoints.push(param);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If the datapoint is not found, we need to find the last datapoint before the current time
|
||||||
|
let lastData;
|
||||||
|
const data = dataset.data || [];
|
||||||
|
for (let i = data.length - 1; i >= 0; i--) {
|
||||||
|
const point = data[i];
|
||||||
|
if (point && point[0] <= time && point[1]) {
|
||||||
|
lastData = point;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lastData) return;
|
||||||
|
datapoints.push({
|
||||||
|
seriesName: dataset.name,
|
||||||
|
seriesIndex: index,
|
||||||
|
value: lastData,
|
||||||
|
// HTML copied from echarts. May change based on options
|
||||||
|
marker: `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const unit = this.unit
|
||||||
|
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
title +
|
||||||
|
datapoints
|
||||||
|
.map((param) => {
|
||||||
|
const entityId = this._entityIds[param.seriesIndex];
|
||||||
|
const stateObj = this.hass.states[entityId];
|
||||||
|
const entry = this.hass.entities[entityId];
|
||||||
|
const stateValue = String(param.value[1]);
|
||||||
|
let value = stateObj
|
||||||
|
? this.hass.formatEntityState(stateObj, stateValue)
|
||||||
|
: `${formatNumber(
|
||||||
|
stateValue,
|
||||||
|
this.hass.locale,
|
||||||
|
getNumberFormatOptions(undefined, entry)
|
||||||
|
)}${unit}`;
|
||||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||||
const data = this.data[dataIndex];
|
const data = this.data[dataIndex];
|
||||||
if (data.statistics && data.statistics.length > 0) {
|
if (data.statistics && data.statistics.length > 0) {
|
||||||
@@ -115,18 +168,21 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
value += source;
|
value += source;
|
||||||
}
|
}
|
||||||
|
|
||||||
const time =
|
if (param.seriesName) {
|
||||||
index === 0
|
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||||
? formatDateTimeWithSeconds(
|
}
|
||||||
new Date(param.value[0]),
|
return `${param.marker} ${value}`;
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config
|
|
||||||
) + "<br>"
|
|
||||||
: "";
|
|
||||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
|
||||||
`;
|
|
||||||
})
|
})
|
||||||
.join("<br>");
|
.join("<br>")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _datasetHidden(ev: CustomEvent) {
|
||||||
|
this._hiddenStats.add(ev.detail.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _datasetUnhidden(ev: CustomEvent) {
|
||||||
|
this._hiddenStats.delete(ev.detail.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public willUpdate(changedProps: PropertyValues) {
|
public willUpdate(changedProps: PropertyValues) {
|
||||||
@@ -156,49 +212,44 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
changedProps.has("paddingYAxis") ||
|
changedProps.has("paddingYAxis") ||
|
||||||
changedProps.has("_yWidth")
|
changedProps.has("_yWidth")
|
||||||
) {
|
) {
|
||||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
|
||||||
const rtl = computeRTL(this.hass);
|
const rtl = computeRTL(this.hass);
|
||||||
const splitLineStyle = this.hass.themes?.darkMode
|
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||||
? { opacity: 0.15 }
|
this.minYAxis;
|
||||||
: {};
|
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||||
|
this.maxYAxis;
|
||||||
|
if (typeof minYAxis === "number") {
|
||||||
|
if (this.fitYData) {
|
||||||
|
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||||
|
}
|
||||||
|
} else if (this.logarithmicScale) {
|
||||||
|
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||||
|
}
|
||||||
|
if (typeof maxYAxis === "number") {
|
||||||
|
if (this.fitYData) {
|
||||||
|
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||||
|
}
|
||||||
|
} else if (this.logarithmicScale) {
|
||||||
|
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||||
|
}
|
||||||
this._chartOptions = {
|
this._chartOptions = {
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "time",
|
type: "time",
|
||||||
min: this.startTime,
|
min: this.startTime,
|
||||||
max: this.endTime,
|
max: this.endTime,
|
||||||
axisLabel: getTimeAxisLabelConfig(
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config,
|
|
||||||
dayDifference
|
|
||||||
),
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: splitLineStyle,
|
|
||||||
},
|
|
||||||
minInterval:
|
|
||||||
dayDifference >= 89 // quarter
|
|
||||||
? 28 * 3600 * 24 * 1000
|
|
||||||
: dayDifference > 2
|
|
||||||
? 3600 * 24 * 1000
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: this.logarithmicScale ? "log" : "value",
|
type: this.logarithmicScale ? "log" : "value",
|
||||||
name: this.unit,
|
name: this.unit,
|
||||||
min: this.fitYData ? this.minYAxis : undefined,
|
min: this._clampYAxis(minYAxis),
|
||||||
max: this.fitYData ? this.maxYAxis : undefined,
|
max: this._clampYAxis(maxYAxis),
|
||||||
position: rtl ? "right" : "left",
|
position: rtl ? "right" : "left",
|
||||||
scale: true,
|
scale: true,
|
||||||
nameGap: 2,
|
nameGap: 2,
|
||||||
nameTextStyle: {
|
nameTextStyle: {
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
splitLine: {
|
axisLine: {
|
||||||
show: true,
|
show: false,
|
||||||
lineStyle: splitLineStyle,
|
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
margin: 5,
|
margin: 5,
|
||||||
@@ -218,6 +269,8 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
} as YAXisOption,
|
} as YAXisOption,
|
||||||
legend: {
|
legend: {
|
||||||
show: this.showNames,
|
show: this.showNames,
|
||||||
|
type: "scroll",
|
||||||
|
animationDurationUpdate: 400,
|
||||||
icon: "circle",
|
icon: "circle",
|
||||||
padding: [20, 0],
|
padding: [20, 0],
|
||||||
},
|
},
|
||||||
@@ -307,13 +360,18 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
prevValues = datavalues;
|
prevValues = datavalues;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addDataSet = (nameY: string, color?: string, fill = false) => {
|
const addDataSet = (
|
||||||
|
id: string,
|
||||||
|
nameY: string,
|
||||||
|
color?: string,
|
||||||
|
fill = false
|
||||||
|
) => {
|
||||||
if (!color) {
|
if (!color) {
|
||||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||||
colorIndex++;
|
colorIndex++;
|
||||||
}
|
}
|
||||||
data.push({
|
data.push({
|
||||||
id: nameY,
|
id,
|
||||||
data: [],
|
data: [],
|
||||||
type: "line",
|
type: "line",
|
||||||
cursor: "default",
|
cursor: "default",
|
||||||
@@ -321,6 +379,7 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
color,
|
color,
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
step: "end",
|
step: "end",
|
||||||
|
animationDurationUpdate: 0,
|
||||||
symbolSize: 1,
|
symbolSize: 1,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: fill ? 0 : 1.5,
|
width: fill ? 0 : 1.5,
|
||||||
@@ -375,13 +434,23 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
entityState.attributes.target_temp_low
|
entityState.attributes.target_temp_low
|
||||||
);
|
);
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.current_temperature", {
|
states.entity_id + "-current_temperature",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.climate.current_temperature", {
|
||||||
name: name,
|
name: name,
|
||||||
})}`
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if (hasHeat) {
|
if (hasHeat) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
|
states.entity_id + "-heating",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||||
|
),
|
||||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -390,7 +459,12 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
}
|
}
|
||||||
if (hasCool) {
|
if (hasCool) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
|
states.entity_id + "-cooling",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||||
|
),
|
||||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -400,22 +474,40 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
if (hasTargetRange) {
|
if (hasTargetRange) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
states.entity_id + "-target_temperature_mode",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||||
name: name,
|
name: name,
|
||||||
mode: this.hass.localize("ui.card.climate.high"),
|
mode: this.hass.localize("ui.card.climate.high"),
|
||||||
})}`
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.target_temp_high.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
|
states.entity_id + "-target_temperature_mode_low",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.climate.target_temperature_mode", {
|
||||||
name: name,
|
name: name,
|
||||||
mode: this.hass.localize("ui.card.climate.low"),
|
mode: this.hass.localize("ui.card.climate.low"),
|
||||||
})}`
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.target_temp_low.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
|
states.entity_id + "-target_temperature",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.card.climate.target_temperature_entity",
|
||||||
|
{
|
||||||
name: name,
|
name: name,
|
||||||
})}`
|
}
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.climate.entity_component._.state_attributes.temperature.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,19 +560,29 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
states.entity_id + "-target_humidity",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.humidifier.target_humidity_entity", {
|
||||||
name: name,
|
name: name,
|
||||||
})}`
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.humidifier.entity_component._.state_attributes.humidity.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasCurrent) {
|
if (hasCurrent) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize(
|
states.entity_id + "-current_humidity",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize(
|
||||||
"ui.card.humidifier.current_humidity_entity",
|
"ui.card.humidifier.current_humidity_entity",
|
||||||
{
|
{
|
||||||
name: name,
|
name: name,
|
||||||
}
|
}
|
||||||
)}`
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.humidifier.entity_component._.state_attributes.current_humidity.name"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,25 +590,40 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
// If action attribute is not available, we shade the area when the device is on
|
// If action attribute is not available, we shade the area when the device is on
|
||||||
if (hasHumidifying) {
|
if (hasHumidifying) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.humidifier.humidifying", {
|
states.entity_id + "-humidifying",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.humidifier.humidifying", {
|
||||||
name: name,
|
name: name,
|
||||||
})}`,
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.humidifier.entity_component._.state_attributes.action.state.humidifying"
|
||||||
|
),
|
||||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else if (hasDrying) {
|
} else if (hasDrying) {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.humidifier.drying", {
|
states.entity_id + "-drying",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.humidifier.drying", {
|
||||||
name: name,
|
name: name,
|
||||||
})}`,
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.humidifier.entity_component._.state_attributes.action.state.drying"
|
||||||
|
),
|
||||||
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
computedStyles.getPropertyValue("--state-humidifier-on-color"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
addDataSet(
|
addDataSet(
|
||||||
`${this.hass.localize("ui.card.humidifier.on_entity", {
|
states.entity_id + "-on",
|
||||||
|
this.showNames
|
||||||
|
? this.hass.localize("ui.card.humidifier.on_entity", {
|
||||||
name: name,
|
name: name,
|
||||||
})}`,
|
})
|
||||||
|
: this.hass.localize(
|
||||||
|
"component.humidifier.entity_component._.state.on"
|
||||||
|
),
|
||||||
undefined,
|
undefined,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -539,7 +656,7 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
pushData(new Date(entityState.last_changed), series);
|
pushData(new Date(entityState.last_changed), series);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addDataSet(name);
|
addDataSet(states.entity_id, name);
|
||||||
|
|
||||||
let lastValue: number;
|
let lastValue: number;
|
||||||
let lastDate: Date;
|
let lastDate: Date;
|
||||||
@@ -609,6 +726,19 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
this._entityIds = entityIds;
|
this._entityIds = entityIds;
|
||||||
this._datasetToDataIndex = datasetToDataIndex;
|
this._datasetToDataIndex = datasetToDataIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||||
|
if (this.logarithmicScale) {
|
||||||
|
// log(0) is -Infinity, so we need to set a minimum value
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Math.max(value, 0.1);
|
||||||
|
}
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return (values: any) => Math.max(value(values), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type {
|
|||||||
TooltipFormatterCallback,
|
TooltipFormatterCallback,
|
||||||
TooltipPositionCallbackParams,
|
TooltipPositionCallbackParams,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
import { differenceInDays } from "date-fns";
|
|
||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
import millisecondsToDuration from "../../common/datetime/milliseconds_to_duration";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
@@ -22,7 +21,6 @@ import { luminosity } from "../../common/color/rgb";
|
|||||||
import { hex2rgb } from "../../common/color/convert-color";
|
import { hex2rgb } from "../../common/color/convert-color";
|
||||||
import { measureTextWidth } from "../../util/text";
|
import { measureTextWidth } from "../../util/text";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
|
||||||
|
|
||||||
@customElement("state-history-chart-timeline")
|
@customElement("state-history-chart-timeline")
|
||||||
export class StateHistoryChartTimeline extends LitElement {
|
export class StateHistoryChartTimeline extends LitElement {
|
||||||
@@ -67,7 +65,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.options=${this._chartOptions}
|
.options=${this._chartOptions}
|
||||||
.height=${`${this.data.length * 30 + 30}px`}
|
.height=${`${this.data.length * 30 + 30}px`}
|
||||||
.data=${this._chartData}
|
.data=${this._chartData as ECOption["series"]}
|
||||||
@chart-click=${this._handleChartClick}
|
@chart-click=${this._handleChartClick}
|
||||||
></ha-chart-base>
|
></ha-chart-base>
|
||||||
`;
|
`;
|
||||||
@@ -129,10 +127,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
|
|
||||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||||
(params: TooltipPositionCallbackParams) => {
|
(params: TooltipPositionCallbackParams) => {
|
||||||
const { value, name, marker } = Array.isArray(params)
|
const { value, name, marker, seriesName } = Array.isArray(params)
|
||||||
? params[0]
|
? params[0]
|
||||||
: params;
|
: params;
|
||||||
const title = `<h4 style="text-align: center; margin: 0;">${value![0]}</h4>`;
|
const title = seriesName
|
||||||
|
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||||
|
: "";
|
||||||
const durationInMs = value![2] - value![1];
|
const durationInMs = value![2] - value![1];
|
||||||
const formattedDuration = `${this.hass.localize(
|
const formattedDuration = `${this.hass.localize(
|
||||||
"ui.components.history_charts.duration"
|
"ui.components.history_charts.duration"
|
||||||
@@ -183,13 +183,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
const narrow = this.narrow;
|
const narrow = this.narrow;
|
||||||
const showNames = this.chunked || this.showNames;
|
const showNames = this.chunked || this.showNames;
|
||||||
const maxInternalLabelWidth = narrow ? 70 : 165;
|
const maxInternalLabelWidth = narrow ? 105 : 185;
|
||||||
const labelWidth = showNames
|
const labelWidth = showNames
|
||||||
? Math.max(this.paddingYAxis, this._yWidth)
|
? Math.max(this.paddingYAxis, this._yWidth)
|
||||||
: 0;
|
: 0;
|
||||||
const labelMargin = 5;
|
const labelMargin = 5;
|
||||||
const rtl = computeRTL(this.hass);
|
const rtl = computeRTL(this.hass);
|
||||||
const dayDifference = differenceInDays(this.endTime, this.startTime);
|
|
||||||
this._chartOptions = {
|
this._chartOptions = {
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "time",
|
type: "time",
|
||||||
@@ -197,21 +196,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
max: this.endTime,
|
max: this.endTime,
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: {
|
|
||||||
opacity: 0.4,
|
|
||||||
},
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false,
|
||||||
},
|
},
|
||||||
axisLabel: getTimeAxisLabelConfig(
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config,
|
|
||||||
dayDifference
|
|
||||||
),
|
|
||||||
minInterval:
|
|
||||||
dayDifference >= 89 // quarter
|
|
||||||
? 28 * 3600 * 24 * 1000
|
|
||||||
: dayDifference > 2
|
|
||||||
? 3600 * 24 * 1000
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
@@ -226,14 +214,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
show: showNames,
|
show: showNames,
|
||||||
width: labelWidth - labelMargin,
|
width: labelWidth,
|
||||||
overflow: "truncate",
|
overflow: "truncate",
|
||||||
margin: labelMargin,
|
margin: labelMargin,
|
||||||
formatter: (label: string) => {
|
formatter: (id: string) => {
|
||||||
const width = Math.min(
|
const label = this._chartData.find((d) => d.id === id)
|
||||||
|
?.name as string;
|
||||||
|
const width = label
|
||||||
|
? Math.min(
|
||||||
measureTextWidth(label, 12) + labelMargin,
|
measureTextWidth(label, 12) + labelMargin,
|
||||||
maxInternalLabelWidth
|
maxInternalLabelWidth
|
||||||
);
|
)
|
||||||
|
: 0;
|
||||||
if (width > this._yWidth) {
|
if (width > this._yWidth) {
|
||||||
this._yWidth = width;
|
this._yWidth = width;
|
||||||
fireEvent(this, "y-width-changed", {
|
fireEvent(this, "y-width-changed", {
|
||||||
@@ -278,8 +270,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
let prevState: string | null = null;
|
let prevState: string | null = null;
|
||||||
let locState: string | null = null;
|
let locState: string | null = null;
|
||||||
let prevLastChanged = startTime;
|
let prevLastChanged = startTime;
|
||||||
const entityDisplay: string =
|
const entityDisplay: string = this.showNames
|
||||||
names[stateInfo.entity_id] || stateInfo.name;
|
? names[stateInfo.entity_id] || stateInfo.name || stateInfo.entity_id
|
||||||
|
: "";
|
||||||
|
|
||||||
const dataRow: unknown[] = [];
|
const dataRow: unknown[] = [];
|
||||||
stateInfo.data.forEach((entityState) => {
|
stateInfo.data.forEach((entityState) => {
|
||||||
@@ -307,7 +300,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
);
|
);
|
||||||
dataRow.push({
|
dataRow.push({
|
||||||
value: [
|
value: [
|
||||||
entityDisplay,
|
stateInfo.entity_id,
|
||||||
prevLastChanged,
|
prevLastChanged,
|
||||||
newLastChanged,
|
newLastChanged,
|
||||||
locState,
|
locState,
|
||||||
@@ -333,7 +326,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
);
|
);
|
||||||
dataRow.push({
|
dataRow.push({
|
||||||
value: [
|
value: [
|
||||||
entityDisplay,
|
stateInfo.entity_id,
|
||||||
prevLastChanged,
|
prevLastChanged,
|
||||||
endTime,
|
endTime,
|
||||||
locState,
|
locState,
|
||||||
@@ -346,9 +339,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
datasets.push({
|
datasets.push({
|
||||||
|
id: stateInfo.entity_id,
|
||||||
data: dataRow,
|
data: dataRow,
|
||||||
name: entityDisplay,
|
name: entityDisplay,
|
||||||
dimensions: ["index", "start", "end", "name", "color", "textColor"],
|
dimensions: ["id", "start", "end", "name", "color", "textColor"],
|
||||||
type: "custom",
|
type: "custom",
|
||||||
encode: {
|
encode: {
|
||||||
x: [1, 2],
|
x: [1, 2],
|
||||||
@@ -364,10 +358,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
|
|
||||||
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
private _handleChartClick(e: CustomEvent<ECElementEvent>): void {
|
||||||
if (e.detail.targetType === "axisLabel") {
|
if (e.detail.targetType === "axisLabel") {
|
||||||
const dataset = this.data[e.detail.dataIndex];
|
const dataset = this._chartData[e.detail.dataIndex];
|
||||||
if (dataset) {
|
if (dataset) {
|
||||||
fireEvent(this, "hass-more-info", {
|
fireEvent(this, "hass-more-info", {
|
||||||
entityId: dataset.entity_id,
|
entityId: dataset.id as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(item)) {
|
if (!Array.isArray(item)) {
|
||||||
return html`<div class="entry-container">
|
return html`<div class="entry-container line">
|
||||||
<state-history-chart-line
|
<state-history-chart-line
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.unit=${item.unit}
|
.unit=${item.unit}
|
||||||
@@ -157,7 +157,7 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
></state-history-chart-line>
|
></state-history-chart-line>
|
||||||
</div> `;
|
</div> `;
|
||||||
}
|
}
|
||||||
return html`<div class="entry-container">
|
return html`<div class="entry-container timeline">
|
||||||
<state-history-chart-timeline
|
<state-history-chart-timeline
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${item}
|
.data=${item}
|
||||||
@@ -299,6 +299,9 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
|
|
||||||
.entry-container {
|
.entry-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-container.line {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +316,10 @@ export class StateHistoryCharts extends LitElement {
|
|||||||
padding-inline-end: 1px;
|
padding-inline-end: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-container.timeline:first-child {
|
||||||
|
margin-top: var(--timeline-top-margin);
|
||||||
|
}
|
||||||
|
|
||||||
.entry-container:not(:first-child) {
|
.entry-container:not(:first-child) {
|
||||||
border-top: 2px solid var(--divider-color);
|
border-top: 2px solid var(--divider-color);
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import type { PropertyValues, TemplateResult } from "lit";
|
|
||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import type {
|
import type {
|
||||||
BarSeriesOption,
|
BarSeriesOption,
|
||||||
LineSeriesOption,
|
LineSeriesOption,
|
||||||
} from "echarts/types/dist/shared";
|
} from "echarts/types/dist/shared";
|
||||||
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
|
||||||
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
|
import {
|
||||||
|
formatNumber,
|
||||||
|
getNumberFormatOptions,
|
||||||
|
} from "../../common/number/format_number";
|
||||||
|
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||||
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import type {
|
import type {
|
||||||
Statistics,
|
Statistics,
|
||||||
StatisticsMetaData,
|
StatisticsMetaData,
|
||||||
@@ -21,16 +28,9 @@ import {
|
|||||||
getStatisticMetadata,
|
getStatisticMetadata,
|
||||||
statisticsHaveType,
|
statisticsHaveType,
|
||||||
} from "../../data/recorder";
|
} from "../../data/recorder";
|
||||||
|
import type { ECOption } from "../../resources/echarts";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "./ha-chart-base";
|
import "./ha-chart-base";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
|
||||||
import type { ECOption } from "../../resources/echarts";
|
|
||||||
import {
|
|
||||||
formatNumber,
|
|
||||||
getNumberFormatOptions,
|
|
||||||
} from "../../common/number/format_number";
|
|
||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
|
||||||
import { getTimeAxisLabelConfig } from "./axis-label";
|
|
||||||
|
|
||||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||||
mean: "mean",
|
mean: "mean",
|
||||||
@@ -56,6 +56,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
|
|
||||||
@property() public unit?: string;
|
@property() public unit?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public startTime?: Date;
|
||||||
|
|
||||||
@property({ attribute: false }) public endTime?: Date;
|
@property({ attribute: false }) public endTime?: Date;
|
||||||
|
|
||||||
@property({ attribute: false, type: Array })
|
@property({ attribute: false, type: Array })
|
||||||
@@ -124,7 +126,10 @@ export class StatisticsChart extends LitElement {
|
|||||||
changedProps.has("fitYData") ||
|
changedProps.has("fitYData") ||
|
||||||
changedProps.has("logarithmicScale") ||
|
changedProps.has("logarithmicScale") ||
|
||||||
changedProps.has("hideLegend") ||
|
changedProps.has("hideLegend") ||
|
||||||
changedProps.has("_legendData")
|
changedProps.has("startTime") ||
|
||||||
|
changedProps.has("endTime") ||
|
||||||
|
changedProps.has("_legendData") ||
|
||||||
|
changedProps.has("_chartData")
|
||||||
) {
|
) {
|
||||||
this._createOptions();
|
this._createOptions();
|
||||||
}
|
}
|
||||||
@@ -181,18 +186,31 @@ export class StatisticsChart extends LitElement {
|
|||||||
this.requestUpdate("_hiddenStats");
|
this.requestUpdate("_hiddenStats");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderTooltip = (params: any) =>
|
private _renderTooltip = (params: any) => {
|
||||||
params
|
const rendered: Record<string, boolean> = {};
|
||||||
|
const unit = this.unit
|
||||||
|
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||||
|
: "";
|
||||||
|
return params
|
||||||
.map((param, index: number) => {
|
.map((param, index: number) => {
|
||||||
const value = `${formatNumber(
|
if (rendered[param.seriesName]) return "";
|
||||||
|
rendered[param.seriesName] = true;
|
||||||
|
|
||||||
|
const statisticId = this._statisticIds[param.seriesIndex];
|
||||||
|
const stateObj = this.hass.states[statisticId];
|
||||||
|
const entry = this.hass.entities[statisticId];
|
||||||
// max series can have 3 values, as the second value is the max-min to form a band
|
// max series can have 3 values, as the second value is the max-min to form a band
|
||||||
(param.value[2] ?? param.value[1]) as number,
|
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||||
|
|
||||||
|
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = `${formatNumber(
|
||||||
|
rawValue,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
getNumberFormatOptions(
|
options
|
||||||
undefined,
|
)}${unit}`;
|
||||||
this.hass.entities[this._statisticIds[param.seriesIndex]]
|
|
||||||
)
|
|
||||||
)} ${this.unit}`;
|
|
||||||
|
|
||||||
const time =
|
const time =
|
||||||
index === 0
|
index === 0
|
||||||
@@ -202,36 +220,68 @@ export class StatisticsChart extends LitElement {
|
|||||||
this.hass.config
|
this.hass.config
|
||||||
) + "<br>"
|
) + "<br>"
|
||||||
: "";
|
: "";
|
||||||
return `${time}${param.marker} ${param.seriesName}: ${value}
|
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||||
`;
|
|
||||||
})
|
})
|
||||||
|
.filter(Boolean)
|
||||||
.join("<br>");
|
.join("<br>");
|
||||||
|
};
|
||||||
|
|
||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
const splitLineStyle = this.hass.themes?.darkMode ? { opacity: 0.15 } : {};
|
|
||||||
const dayDifference = this.daysToShow ?? 1;
|
const dayDifference = this.daysToShow ?? 1;
|
||||||
|
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||||
|
this.minYAxis;
|
||||||
|
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||||
|
this.maxYAxis;
|
||||||
|
if (typeof minYAxis === "number") {
|
||||||
|
if (this.fitYData) {
|
||||||
|
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
|
||||||
|
}
|
||||||
|
} else if (this.logarithmicScale) {
|
||||||
|
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
|
||||||
|
}
|
||||||
|
if (typeof maxYAxis === "number") {
|
||||||
|
if (this.fitYData) {
|
||||||
|
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
|
||||||
|
}
|
||||||
|
} else if (this.logarithmicScale) {
|
||||||
|
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
|
||||||
|
}
|
||||||
|
const endTime = this.endTime ?? new Date();
|
||||||
|
let startTime = this.startTime;
|
||||||
|
|
||||||
|
if (!startTime) {
|
||||||
|
// set start time to the earliest point in the chart data
|
||||||
|
this._chartData.forEach((series) => {
|
||||||
|
if (!Array.isArray(series.data) || !series.data[0]) return;
|
||||||
|
const firstPoint = series.data[0] as any;
|
||||||
|
const timestamp = Array.isArray(firstPoint)
|
||||||
|
? firstPoint[0]
|
||||||
|
: firstPoint.value?.[0];
|
||||||
|
if (timestamp && (!startTime || new Date(timestamp) < startTime)) {
|
||||||
|
startTime = new Date(timestamp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!startTime) {
|
||||||
|
// Calculate default start time based on dayDifference
|
||||||
|
startTime = new Date(
|
||||||
|
endTime.getTime() - dayDifference * 24 * 3600 * 1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._chartOptions = {
|
this._chartOptions = {
|
||||||
xAxis: {
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: "time",
|
||||||
|
min: startTime,
|
||||||
|
max: endTime,
|
||||||
|
},
|
||||||
|
{
|
||||||
type: "time",
|
type: "time",
|
||||||
axisLabel: getTimeAxisLabelConfig(
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config,
|
|
||||||
dayDifference
|
|
||||||
),
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
splitLine: {
|
],
|
||||||
show: true,
|
|
||||||
lineStyle: splitLineStyle,
|
|
||||||
},
|
|
||||||
minInterval:
|
|
||||||
dayDifference >= 89 // quarter
|
|
||||||
? 28 * 3600 * 24 * 1000
|
|
||||||
: dayDifference > 2
|
|
||||||
? 3600 * 24 * 1000
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: this.logarithmicScale ? "log" : "value",
|
type: this.logarithmicScale ? "log" : "value",
|
||||||
name: this.unit,
|
name: this.unit,
|
||||||
@@ -240,24 +290,24 @@ export class StatisticsChart extends LitElement {
|
|||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
position: computeRTL(this.hass) ? "right" : "left",
|
position: computeRTL(this.hass) ? "right" : "left",
|
||||||
// @ts-ignore
|
scale: true,
|
||||||
scale: this.chartType !== "bar",
|
min: this._clampYAxis(minYAxis),
|
||||||
min: this.fitYData ? undefined : this.minYAxis,
|
max: this._clampYAxis(maxYAxis),
|
||||||
max: this.fitYData ? undefined : this.maxYAxis,
|
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
lineStyle: splitLineStyle,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: !this.hideLegend,
|
show: !this.hideLegend,
|
||||||
|
type: "scroll",
|
||||||
|
animationDurationUpdate: 400,
|
||||||
icon: "circle",
|
icon: "circle",
|
||||||
padding: [20, 0],
|
padding: [20, 0],
|
||||||
data: this._legendData,
|
data: this._legendData,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
|
...(this.hideLegend ? { top: this.unit ? 30 : 5 } : {}), // undefined is the same as 0
|
||||||
left: 20,
|
left: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
@@ -318,6 +368,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
if (endTime > new Date()) {
|
if (endTime > new Date()) {
|
||||||
endTime = new Date();
|
endTime = new Date();
|
||||||
}
|
}
|
||||||
|
this.endTime = endTime;
|
||||||
|
|
||||||
let unit: string | undefined | null;
|
let unit: string | undefined | null;
|
||||||
|
|
||||||
@@ -369,10 +420,12 @@ export class StatisticsChart extends LitElement {
|
|||||||
) {
|
) {
|
||||||
// if the end of the previous data doesn't match the start of the current data,
|
// if the end of the previous data doesn't match the start of the current data,
|
||||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
d.data!.push(
|
||||||
|
this._transformDataValue([prevEndTime, ...prevValues[i]!])
|
||||||
|
);
|
||||||
d.data!.push([prevEndTime, null]);
|
d.data!.push([prevEndTime, null]);
|
||||||
}
|
}
|
||||||
d.data!.push([start, ...dataValues[i]!]);
|
d.data!.push(this._transformDataValue([start, ...dataValues[i]!]));
|
||||||
});
|
});
|
||||||
prevValues = dataValues;
|
prevValues = dataValues;
|
||||||
prevEndTime = end;
|
prevEndTime = end;
|
||||||
@@ -421,9 +474,14 @@ export class StatisticsChart extends LitElement {
|
|||||||
displayedLegend = displayedLegend || showLegend;
|
displayedLegend = displayedLegend || showLegend;
|
||||||
}
|
}
|
||||||
statTypes.push(type);
|
statTypes.push(type);
|
||||||
|
const borderColor =
|
||||||
|
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
|
||||||
|
const backgroundColor = band ? color + "3F" : color + "7F";
|
||||||
const series: LineSeriesOption | BarSeriesOption = {
|
const series: LineSeriesOption | BarSeriesOption = {
|
||||||
id: `${statistic_id}-${type}`,
|
id: `${statistic_id}-${type}`,
|
||||||
type: this.chartType,
|
type: this.chartType,
|
||||||
|
smooth: this.chartType === "line" ? 0.4 : false,
|
||||||
|
smoothMonotone: "x",
|
||||||
cursor: "default",
|
cursor: "default",
|
||||||
data: [],
|
data: [],
|
||||||
name: name
|
name: name
|
||||||
@@ -435,6 +493,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
),
|
),
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
symbolSize: 0,
|
symbolSize: 0,
|
||||||
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
},
|
},
|
||||||
@@ -442,21 +501,16 @@ export class StatisticsChart extends LitElement {
|
|||||||
this.chartType === "bar"
|
this.chartType === "bar"
|
||||||
? {
|
? {
|
||||||
borderRadius: [4, 4, 0, 0],
|
borderRadius: [4, 4, 0, 0],
|
||||||
borderColor:
|
borderColor,
|
||||||
band && hasMean
|
|
||||||
? color + (this.hideLegend ? "00" : "7F")
|
|
||||||
: color,
|
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
color: band ? color + "3F" : color + "7F",
|
color: this.chartType === "bar" ? backgroundColor : borderColor,
|
||||||
};
|
};
|
||||||
if (band && this.chartType === "line") {
|
if (band && this.chartType === "line") {
|
||||||
series.stack = `band-${statistic_id}`;
|
series.stack = `band-${statistic_id}`;
|
||||||
|
series.stackStrategy = "all";
|
||||||
(series as LineSeriesOption).symbol = "none";
|
(series as LineSeriesOption).symbol = "none";
|
||||||
(series as LineSeriesOption).lineStyle = {
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
if (drawBands && type === "max") {
|
if (drawBands && type === "max") {
|
||||||
(series as LineSeriesOption).areaStyle = {
|
(series as LineSeriesOption).areaStyle = {
|
||||||
color: color + "3F",
|
color: color + "3F",
|
||||||
@@ -489,7 +543,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
}
|
}
|
||||||
} else if (type === "max" && this.chartType === "line") {
|
} else if (type === "max" && this.chartType === "line") {
|
||||||
const max = stat.max || 0;
|
const max = stat.max || 0;
|
||||||
val.push(max - (stat.min || 0));
|
val.push(Math.abs(max - (stat.min || 0)));
|
||||||
val.push(max);
|
val.push(max);
|
||||||
} else {
|
} else {
|
||||||
val.push(stat[type] ?? null);
|
val.push(stat[type] ?? null);
|
||||||
@@ -518,6 +572,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
color,
|
color,
|
||||||
type: this.chartType,
|
type: this.chartType,
|
||||||
data: [],
|
data: [],
|
||||||
|
xAxisIndex: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -529,6 +584,26 @@ export class StatisticsChart extends LitElement {
|
|||||||
this._statisticIds = statisticIds;
|
this._statisticIds = statisticIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _transformDataValue(val: [Date, ...(number | null)[]]) {
|
||||||
|
if (this.chartType === "bar" && val[1] && val[1] < 0) {
|
||||||
|
return { value: val, itemStyle: { borderRadius: [0, 0, 4, 4] } };
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||||
|
if (this.logarithmicScale) {
|
||||||
|
// log(0) is -Infinity, so we need to set a minimum value
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Math.max(value, 0.1);
|
||||||
|
}
|
||||||
|
if (typeof value === "function") {
|
||||||
|
return (values: any) => Math.max(value(values), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -295,10 +295,12 @@ export class HaAssistChat extends LitElement {
|
|||||||
this._addMessage(userMessage);
|
this._addMessage(userMessage);
|
||||||
this.requestUpdate("_audioRecorder");
|
this.requestUpdate("_audioRecorder");
|
||||||
|
|
||||||
const hassMessage: AssistMessage = {
|
let hassMessage = {
|
||||||
who: "hass",
|
who: "hass",
|
||||||
text: "…",
|
text: "…",
|
||||||
|
error: false,
|
||||||
};
|
};
|
||||||
|
let currentDeltaRole = "";
|
||||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
try {
|
try {
|
||||||
const unsub = await runAssistPipeline(
|
const unsub = await runAssistPipeline(
|
||||||
@@ -328,6 +330,43 @@ export class HaAssistChat extends LitElement {
|
|||||||
this._addMessage(hassMessage);
|
this._addMessage(hassMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.type === "intent-progress") {
|
||||||
|
const delta = event.data.chat_log_delta;
|
||||||
|
|
||||||
|
// new message
|
||||||
|
if (delta.role) {
|
||||||
|
// If currentDeltaRole exists, it means we're receiving our
|
||||||
|
// second or later message. Let's add it to the chat.
|
||||||
|
if (currentDeltaRole && delta.role && hassMessage.text !== "…") {
|
||||||
|
// Remove progress indicator of previous message
|
||||||
|
hassMessage.text = hassMessage.text.substring(
|
||||||
|
0,
|
||||||
|
hassMessage.text.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
hassMessage = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
this._addMessage(hassMessage);
|
||||||
|
}
|
||||||
|
currentDeltaRole = delta.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentDeltaRole === "assistant" &&
|
||||||
|
"content" in delta &&
|
||||||
|
delta.content
|
||||||
|
) {
|
||||||
|
hassMessage.text =
|
||||||
|
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||||
|
delta.content +
|
||||||
|
"…";
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "intent-end") {
|
if (event.type === "intent-end") {
|
||||||
this._conversationId = event.data.intent_output.conversation_id;
|
this._conversationId = event.data.intent_output.conversation_id;
|
||||||
const plain = event.data.intent_output.response.speech?.plain;
|
const plain = event.data.intent_output.response.speech?.plain;
|
||||||
@@ -435,28 +474,71 @@ export class HaAssistChat extends LitElement {
|
|||||||
this._processing = true;
|
this._processing = true;
|
||||||
this._audio?.pause();
|
this._audio?.pause();
|
||||||
this._addMessage({ who: "user", text });
|
this._addMessage({ who: "user", text });
|
||||||
const message: AssistMessage = {
|
let hassMessage = {
|
||||||
who: "hass",
|
who: "hass",
|
||||||
text: "…",
|
text: "…",
|
||||||
|
error: false,
|
||||||
};
|
};
|
||||||
|
let currentDeltaRole = "";
|
||||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
this._addMessage(message);
|
this._addMessage(hassMessage);
|
||||||
try {
|
try {
|
||||||
const unsub = await runAssistPipeline(
|
const unsub = await runAssistPipeline(
|
||||||
this.hass,
|
this.hass,
|
||||||
(event) => {
|
(event) => {
|
||||||
|
if (event.type === "intent-progress") {
|
||||||
|
const delta = event.data.chat_log_delta;
|
||||||
|
|
||||||
|
// new message and previous message has content
|
||||||
|
if (delta.role) {
|
||||||
|
// If currentDeltaRole exists, it means we're receiving our
|
||||||
|
// second or later message. Let's add it to the chat.
|
||||||
|
if (
|
||||||
|
currentDeltaRole &&
|
||||||
|
delta.role === "assistant" &&
|
||||||
|
hassMessage.text !== "…"
|
||||||
|
) {
|
||||||
|
// Remove progress indicator of previous message
|
||||||
|
hassMessage.text = hassMessage.text.substring(
|
||||||
|
0,
|
||||||
|
hassMessage.text.length - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
hassMessage = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
this._addMessage(hassMessage);
|
||||||
|
}
|
||||||
|
currentDeltaRole = delta.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentDeltaRole === "assistant" &&
|
||||||
|
"content" in delta &&
|
||||||
|
delta.content
|
||||||
|
) {
|
||||||
|
hassMessage.text =
|
||||||
|
hassMessage.text.substring(0, hassMessage.text.length - 1) +
|
||||||
|
delta.content +
|
||||||
|
"…";
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "intent-end") {
|
if (event.type === "intent-end") {
|
||||||
this._conversationId = event.data.intent_output.conversation_id;
|
this._conversationId = event.data.intent_output.conversation_id;
|
||||||
const plain = event.data.intent_output.response.speech?.plain;
|
const plain = event.data.intent_output.response.speech?.plain;
|
||||||
if (plain) {
|
if (plain) {
|
||||||
message.text = plain.speech;
|
hassMessage.text = plain.speech;
|
||||||
}
|
}
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
unsub();
|
unsub();
|
||||||
}
|
}
|
||||||
if (event.type === "error") {
|
if (event.type === "error") {
|
||||||
message.text = event.data.message;
|
hassMessage.text = event.data.message;
|
||||||
message.error = true;
|
hassMessage.error = true;
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
unsub();
|
unsub();
|
||||||
}
|
}
|
||||||
@@ -470,8 +552,8 @@ export class HaAssistChat extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
hassMessage.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||||
message.error = true;
|
hassMessage.error = true;
|
||||||
this.requestUpdate("_conversation");
|
this.requestUpdate("_conversation");
|
||||||
} finally {
|
} finally {
|
||||||
this._processing = false;
|
this._processing = false;
|
||||||
|
|||||||
@@ -329,15 +329,12 @@ export class HaBaseTimeInput extends LitElement {
|
|||||||
:host([clearable]) {
|
:host([clearable]) {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.time-input-wrap-wrap {
|
.time-input-wrap-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.time-input-wrap {
|
.time-input-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: var(--time-input-flex, unset);
|
||||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
endOfMonth,
|
endOfMonth,
|
||||||
endOfWeek,
|
endOfWeek,
|
||||||
endOfYear,
|
endOfYear,
|
||||||
|
isThisYear,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
startOfWeek,
|
startOfWeek,
|
||||||
startOfYear,
|
startOfYear,
|
||||||
isThisYear,
|
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
|
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||||
import type { PropertyValues, TemplateResult } from "lit";
|
import type { PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@@ -22,16 +23,18 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||||||
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
||||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||||
import {
|
import {
|
||||||
formatShortDateTimeWithYear,
|
|
||||||
formatShortDateTime,
|
formatShortDateTime,
|
||||||
|
formatShortDateTimeWithYear,
|
||||||
} from "../common/datetime/format_date_time";
|
} from "../common/datetime/format_date_time";
|
||||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { TimeZone } from "../data/translation";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./date-range-picker";
|
import "./date-range-picker";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-textarea";
|
|
||||||
import "./ha-icon-button-next";
|
import "./ha-icon-button-next";
|
||||||
import "./ha-icon-button-prev";
|
import "./ha-icon-button-prev";
|
||||||
|
import "./ha-textarea";
|
||||||
|
|
||||||
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
||||||
|
|
||||||
@@ -197,14 +200,15 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
?auto-apply=${this.autoApply}
|
?auto-apply=${this.autoApply}
|
||||||
time-picker=${this.timePicker}
|
time-picker=${this.timePicker}
|
||||||
twentyfour-hours=${this._hour24format}
|
twentyfour-hours=${this._hour24format}
|
||||||
start-date=${this.startDate.toISOString()}
|
start-date=${this._formatDate(this.startDate)}
|
||||||
end-date=${this.endDate.toISOString()}
|
end-date=${this._formatDate(this.endDate)}
|
||||||
?ranges=${this.ranges !== false}
|
?ranges=${this.ranges !== false}
|
||||||
opening-direction=${ifDefined(
|
opening-direction=${ifDefined(
|
||||||
this.openingDirection || this._calcedOpeningDirection
|
this.openingDirection || this._calcedOpeningDirection
|
||||||
)}
|
)}
|
||||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||||
language=${this.hass.locale.language}
|
language=${this.hass.locale.language}
|
||||||
|
@change=${this._handleChange}
|
||||||
>
|
>
|
||||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||||
${!this.minimal
|
${!this.minimal
|
||||||
@@ -325,9 +329,31 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _applyDateRange() {
|
private _applyDateRange() {
|
||||||
|
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||||
|
const dateRangePicker = this._dateRangePicker;
|
||||||
|
|
||||||
|
const startDate = fromZonedTime(
|
||||||
|
dateRangePicker.start,
|
||||||
|
this.hass.config.time_zone
|
||||||
|
);
|
||||||
|
const endDate = fromZonedTime(
|
||||||
|
dateRangePicker.end,
|
||||||
|
this.hass.config.time_zone
|
||||||
|
);
|
||||||
|
|
||||||
|
dateRangePicker.clickRange([startDate, endDate]);
|
||||||
|
}
|
||||||
|
|
||||||
this._dateRangePicker.clickedApply();
|
this._dateRangePicker.clickedApply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _formatDate(date: Date): string {
|
||||||
|
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||||
|
return toZonedTime(date, this.hass.config.time_zone).toISOString();
|
||||||
|
}
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
private get _dateRangePicker() {
|
private get _dateRangePicker() {
|
||||||
const dateRangePicker = this.shadowRoot!.querySelector(
|
const dateRangePicker = this.shadowRoot!.querySelector(
|
||||||
"date-range-picker"
|
"date-range-picker"
|
||||||
@@ -358,6 +384,16 @@ export class HaDateRangePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleChange(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const startDate = ev.detail.startDate;
|
||||||
|
const endDate = ev.detail.endDate;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: { startDate, endDate },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
|
||||||
ha-icon-button {
|
ha-icon-button {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import "./ha-icon-button";
|
|||||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import { bytesToString } from "../util/bytes-to-string";
|
import { bytesToString } from "../util/bytes-to-string";
|
||||||
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@@ -23,6 +24,8 @@ declare global {
|
|||||||
export class HaFileUpload extends LitElement {
|
export class HaFileUpload extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||||
|
|
||||||
@property() public accept!: string;
|
@property() public accept!: string;
|
||||||
|
|
||||||
@property() public icon?: string;
|
@property() public icon?: string;
|
||||||
@@ -31,6 +34,10 @@ export class HaFileUpload extends LitElement {
|
|||||||
|
|
||||||
@property() public secondary?: string;
|
@property() public secondary?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "delete-label" }) public deleteLabel?: string;
|
||||||
|
|
||||||
@property() public supports?: string;
|
@property() public supports?: string;
|
||||||
|
|
||||||
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
@property({ type: Object }) public value?: File | File[] | FileList | string;
|
||||||
@@ -73,23 +80,22 @@ export class HaFileUpload extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
|
const localize = this.localize || this.hass!.localize;
|
||||||
return html`
|
return html`
|
||||||
${this.uploading
|
${this.uploading
|
||||||
? html`<div class="container">
|
? html`<div class="container">
|
||||||
<div class="uploading">
|
<div class="uploading">
|
||||||
<span class="header"
|
<span class="header"
|
||||||
>${this.value
|
>${this.uploadingLabel || this.value
|
||||||
? this.hass?.localize(
|
? localize("ui.components.file-upload.uploading_name", {
|
||||||
"ui.components.file-upload.uploading_name",
|
name: this._name,
|
||||||
{ name: this._name }
|
})
|
||||||
)
|
: localize("ui.components.file-upload.uploading")}</span
|
||||||
: this.hass?.localize(
|
|
||||||
"ui.components.file-upload.uploading"
|
|
||||||
)}</span
|
|
||||||
>
|
>
|
||||||
${this.progress
|
${this.progress
|
||||||
? html`<div class="progress">
|
? html`<div class="progress">
|
||||||
${this.progress}${blankBeforePercent(this.hass!.locale)}%
|
${this.progress}${this.hass &&
|
||||||
|
blankBeforePercent(this.hass!.locale)}%
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
@@ -116,14 +122,11 @@ export class HaFileUpload extends LitElement {
|
|||||||
.path=${this.icon || mdiFileUpload}
|
.path=${this.icon || mdiFileUpload}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<ha-button unelevated @click=${this._openFilePicker}>
|
<ha-button unelevated @click=${this._openFilePicker}>
|
||||||
${this.label ||
|
${this.label || localize("ui.components.file-upload.label")}
|
||||||
this.hass?.localize("ui.components.file-upload.label")}
|
|
||||||
</ha-button>
|
</ha-button>
|
||||||
<span class="secondary"
|
<span class="secondary"
|
||||||
>${this.secondary ||
|
>${this.secondary ||
|
||||||
this.hass?.localize(
|
localize("ui.components.file-upload.secondary")}</span
|
||||||
"ui.components.file-upload.secondary"
|
|
||||||
)}</span
|
|
||||||
>
|
>
|
||||||
<span class="supports">${this.supports}</span>`
|
<span class="supports">${this.supports}</span>`
|
||||||
: typeof this.value === "string"
|
: typeof this.value === "string"
|
||||||
@@ -136,8 +139,7 @@ export class HaFileUpload extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@click=${this._clearValue}
|
@click=${this._clearValue}
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
.label=${this.deleteLabel || localize("ui.common.delete")}
|
||||||
"Delete"}
|
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -155,8 +157,8 @@ export class HaFileUpload extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@click=${this._clearValue}
|
@click=${this._clearValue}
|
||||||
.label=${this.hass?.localize("ui.common.delete") ||
|
.label=${this.deleteLabel ||
|
||||||
"Delete"}
|
localize("ui.common.delete")}
|
||||||
.path=${mdiDelete}
|
.path=${mdiDelete}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -238,6 +240,10 @@ export class HaFileUpload extends LitElement {
|
|||||||
border-radius: var(--mdc-shape-small, 4px);
|
border-radius: var(--mdc-shape-small, 4px);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
label.container {
|
label.container {
|
||||||
border: dashed 1px
|
border: dashed 1px
|
||||||
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||||
|
|||||||
@@ -64,9 +64,13 @@ export class HaNetwork extends LitElement {
|
|||||||
>
|
>
|
||||||
</ha-checkbox>
|
</ha-checkbox>
|
||||||
</span>
|
</span>
|
||||||
<span slot="heading" data-for="auto_configure"> Auto Configure </span>
|
<span slot="heading" data-for="auto_configure">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.network.adapter.auto_configure"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<span slot="description" data-for="auto_configure">
|
<span slot="description" data-for="auto_configure">
|
||||||
Detected:
|
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||||
</span>
|
</span>
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
@@ -85,18 +89,21 @@ export class HaNetwork extends LitElement {
|
|||||||
</ha-checkbox>
|
</ha-checkbox>
|
||||||
</span>
|
</span>
|
||||||
<span slot="heading">
|
<span slot="heading">
|
||||||
Adapter: ${adapter.name}
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.network.adapter.adapter"
|
||||||
|
)}:
|
||||||
|
${adapter.name}
|
||||||
${adapter.default
|
${adapter.default
|
||||||
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
? html`<ha-svg-icon .path=${mdiStar}></ha-svg-icon>
|
||||||
(Default)`
|
(${this.hass.localize("ui.common.default")})`
|
||||||
: ""}
|
: nothing}
|
||||||
</span>
|
</span>
|
||||||
<span slot="description">
|
<span slot="description">
|
||||||
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
${format_addresses([...adapter.ipv4, ...adapter.ipv6])}
|
||||||
</span>
|
</span>
|
||||||
</ha-settings-row>`
|
</ha-settings-row>`
|
||||||
)
|
)
|
||||||
: ""}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
||||||
// The following import makes a better implementation available that is based on a
|
// The following import makes a better implementation available that is based on a
|
||||||
// WebAssembly port of ZXing:
|
// WebAssembly port of ZXing:
|
||||||
import { setZXingModuleOverrides } from "barcode-detector";
|
import { prepareZXingModule } from "barcode-detector";
|
||||||
import type QrScanner from "qr-scanner";
|
import type QrScanner from "qr-scanner";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
@@ -21,13 +21,15 @@ import "./ha-list-item";
|
|||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import type { HaTextField } from "./ha-textfield";
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
|
||||||
setZXingModuleOverrides({
|
prepareZXingModule({
|
||||||
|
overrides: {
|
||||||
locateFile: (path: string, prefix: string) => {
|
locateFile: (path: string, prefix: string) => {
|
||||||
if (path.endsWith(".wasm")) {
|
if (path.endsWith(".wasm")) {
|
||||||
return "/static/js/zxing_reader.wasm";
|
return "/static/js/zxing_reader.wasm";
|
||||||
}
|
}
|
||||||
return prefix + path;
|
return prefix + path;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@customElement("ha-qr-scanner")
|
@customElement("ha-qr-scanner")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { css, html, LitElement, svg } from "lit";
|
import { css, html, LitElement, nothing, svg } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
|
import { BRANCH_HEIGHT, SPACING } from "./hat-graph-const";
|
||||||
@@ -41,8 +41,8 @@ export class HatGraphBranch extends LitElement {
|
|||||||
branches.push({
|
branches.push({
|
||||||
x: width / 2 + total_width,
|
x: width / 2 + total_width,
|
||||||
height,
|
height,
|
||||||
start: c.hasAttribute("graphStart"),
|
start: c.hasAttribute("graph-start"),
|
||||||
end: c.hasAttribute("graphEnd"),
|
end: c.hasAttribute("graph-end"),
|
||||||
track: c.hasAttribute("track"),
|
track: c.hasAttribute("track"),
|
||||||
});
|
});
|
||||||
total_width += width;
|
total_width += width;
|
||||||
@@ -65,11 +65,8 @@ export class HatGraphBranch extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<slot name="head"></slot>
|
<slot name="head"></slot>
|
||||||
${!this.start
|
${!this.start
|
||||||
? svg`
|
? html`
|
||||||
<svg
|
<svg id="top" width=${this._totalWidth}>
|
||||||
id="top"
|
|
||||||
width="${this._totalWidth}"
|
|
||||||
>
|
|
||||||
${this._branches.map((branch) =>
|
${this._branches.map((branch) =>
|
||||||
branch.start
|
branch.start
|
||||||
? ""
|
? ""
|
||||||
@@ -86,7 +83,7 @@ export class HatGraphBranch extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<div id="branches">
|
<div id="branches">
|
||||||
<svg id="lines" width=${this._totalWidth} height=${this._maxHeight}>
|
<svg id="lines" width=${this._totalWidth} height=${this._maxHeight}>
|
||||||
${this._branches.map((branch) => {
|
${this._branches.map((branch) => {
|
||||||
@@ -107,11 +104,8 @@ export class HatGraphBranch extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
${!this.short
|
${!this.short
|
||||||
? svg`
|
? html`
|
||||||
<svg
|
<svg id="bottom" width=${this._totalWidth}>
|
||||||
id="bottom"
|
|
||||||
width="${this._totalWidth}"
|
|
||||||
>
|
|
||||||
${this._branches.map((branch) => {
|
${this._branches.map((branch) => {
|
||||||
if (branch.end) return "";
|
if (branch.end) return "";
|
||||||
return svg`
|
return svg`
|
||||||
@@ -128,7 +122,7 @@ export class HatGraphBranch extends LitElement {
|
|||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,34 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
|
|||||||
intent_input: string;
|
intent_input: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConversationChatLogAssistantDelta {
|
||||||
|
role: "assistant";
|
||||||
|
content: string;
|
||||||
|
tool_calls: {
|
||||||
|
id: string;
|
||||||
|
tool_name: string;
|
||||||
|
tool_args: Record<string, unknown>;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversationChatLogToolResultDelta {
|
||||||
|
role: "tool_result";
|
||||||
|
agent_id: string;
|
||||||
|
tool_call_id: string;
|
||||||
|
tool_name: string;
|
||||||
|
tool_result: unknown;
|
||||||
|
}
|
||||||
|
interface PipelineIntentProgressEvent extends PipelineEventBase {
|
||||||
|
type: "intent-progress";
|
||||||
|
data: {
|
||||||
|
chat_log_delta:
|
||||||
|
| Partial<ConversationChatLogAssistantDelta>
|
||||||
|
// These always come in 1 chunk
|
||||||
|
| ConversationChatLogToolResultDelta;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface PipelineIntentEndEvent extends PipelineEventBase {
|
interface PipelineIntentEndEvent extends PipelineEventBase {
|
||||||
type: "intent-end";
|
type: "intent-end";
|
||||||
data: {
|
data: {
|
||||||
@@ -141,6 +169,7 @@ export type PipelineRunEvent =
|
|||||||
| PipelineSTTStartEvent
|
| PipelineSTTStartEvent
|
||||||
| PipelineSTTEndEvent
|
| PipelineSTTEndEvent
|
||||||
| PipelineIntentStartEvent
|
| PipelineIntentStartEvent
|
||||||
|
| PipelineIntentProgressEvent
|
||||||
| PipelineIntentEndEvent
|
| PipelineIntentEndEvent
|
||||||
| PipelineTTSStartEvent
|
| PipelineTTSStartEvent
|
||||||
| PipelineTTSEndEvent;
|
| PipelineTTSEndEvent;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memoize } from "@fullcalendar/core/internal";
|
||||||
import { setHours, setMinutes } from "date-fns";
|
import { setHours, setMinutes } from "date-fns";
|
||||||
import type { HassConfig } from "home-assistant-js-websocket";
|
import type { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -12,6 +13,7 @@ import { fileDownload } from "../util/file_download";
|
|||||||
import { domainToName } from "./integration";
|
import { domainToName } from "./integration";
|
||||||
import type { FrontendLocaleData } from "./translation";
|
import type { FrontendLocaleData } from "./translation";
|
||||||
import checkValidDate from "../common/datetime/check_valid_date";
|
import checkValidDate from "../common/datetime/check_valid_date";
|
||||||
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
|
|
||||||
export const enum BackupScheduleRecurrence {
|
export const enum BackupScheduleRecurrence {
|
||||||
NEVER = "never",
|
NEVER = "never",
|
||||||
@@ -104,6 +106,9 @@ export interface BackupContent {
|
|||||||
name: string;
|
name: string;
|
||||||
agents: Record<string, BackupContentAgent>;
|
agents: Record<string, BackupContentAgent>;
|
||||||
failed_agent_ids?: string[];
|
failed_agent_ids?: string[];
|
||||||
|
extra_metadata?: {
|
||||||
|
"supervisor.addon_update"?: string;
|
||||||
|
};
|
||||||
with_automatic_settings: boolean;
|
with_automatic_settings: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,27 +232,23 @@ export const restoreBackup = (
|
|||||||
export const uploadBackup = async (
|
export const uploadBackup = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
file: File,
|
file: File,
|
||||||
agent_ids: string[]
|
agentIds: string[]
|
||||||
): Promise<void> => {
|
): Promise<{ backup_id: string }> => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("file", file);
|
fd.append("file", file);
|
||||||
|
|
||||||
const params = agent_ids.reduce((acc, agent_id) => {
|
const params = new URLSearchParams();
|
||||||
acc.append("agent_id", agent_id);
|
|
||||||
return acc;
|
|
||||||
}, new URLSearchParams());
|
|
||||||
|
|
||||||
const resp = await hass.fetchWithAuth(
|
agentIds.forEach((agentId) => {
|
||||||
`/api/backup/upload?${params.toString()}`,
|
params.append("agent_id", agentId);
|
||||||
{
|
});
|
||||||
|
|
||||||
|
return handleFetchPromise(
|
||||||
|
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: fd,
|
body: fd,
|
||||||
}
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPreferredAgentForDownload = (agents: string[]) => {
|
export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||||
@@ -319,6 +320,29 @@ export const computeBackupAgentName = (
|
|||||||
export const computeBackupSize = (backup: BackupContent) =>
|
export const computeBackupSize = (backup: BackupContent) =>
|
||||||
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
|
Math.max(...Object.values(backup.agents).map((agent) => agent.size));
|
||||||
|
|
||||||
|
export type BackupType = "automatic" | "manual" | "addon_update";
|
||||||
|
|
||||||
|
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "manual", "addon_update"];
|
||||||
|
|
||||||
|
export const getBackupTypes = memoize((isHassio: boolean) =>
|
||||||
|
isHassio
|
||||||
|
? BACKUP_TYPE_ORDER
|
||||||
|
: BACKUP_TYPE_ORDER.filter((type) => type !== "addon_update")
|
||||||
|
);
|
||||||
|
|
||||||
|
export const computeBackupType = (
|
||||||
|
backup: BackupContent,
|
||||||
|
isHassio: boolean
|
||||||
|
): BackupType => {
|
||||||
|
if (backup.with_automatic_settings) {
|
||||||
|
return "automatic";
|
||||||
|
}
|
||||||
|
if (isHassio && backup.extra_metadata?.["supervisor.addon_update"] != null) {
|
||||||
|
return "addon_update";
|
||||||
|
}
|
||||||
|
return "manual";
|
||||||
|
};
|
||||||
|
|
||||||
export const compareAgents = (a: string, b: string) => {
|
export const compareAgents = (a: string, b: string) => {
|
||||||
const isLocalA = isLocalAgent(a);
|
const isLocalA = isLocalAgent(a);
|
||||||
const isLocalB = isLocalAgent(b);
|
const isLocalB = isLocalAgent(b);
|
||||||
@@ -422,3 +446,13 @@ export const getFormattedBackupTime = memoizeOne(
|
|||||||
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
|
return `${formatTime(DEFAULT_OPTIMIZED_BACKUP_START_TIME, locale, config)} - ${formatTime(DEFAULT_OPTIMIZED_BACKUP_END_TIME, locale, config)}`;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SUPPORTED_UPLOAD_FORMAT = "application/x-tar";
|
||||||
|
|
||||||
|
export interface BackupUploadFileFormData {
|
||||||
|
file?: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INITIAL_UPLOAD_FORM_DATA: BackupUploadFileFormData = {
|
||||||
|
file: undefined,
|
||||||
|
};
|
||||||
|
|||||||
66
src/data/backup_onboarding.ts
Normal file
66
src/data/backup_onboarding.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { handleFetchPromise } from "../util/hass-call-api";
|
||||||
|
import type { BackupContentExtended } from "./backup";
|
||||||
|
import type {
|
||||||
|
BackupManagerState,
|
||||||
|
RestoreBackupStage,
|
||||||
|
RestoreBackupState,
|
||||||
|
} from "./backup_manager";
|
||||||
|
|
||||||
|
export interface BackupOnboardingInfo {
|
||||||
|
state: BackupManagerState;
|
||||||
|
last_non_idle_event?: {
|
||||||
|
manager_state: BackupManagerState;
|
||||||
|
stage: RestoreBackupStage | null;
|
||||||
|
state: RestoreBackupState;
|
||||||
|
reason: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupOnboardingConfig extends BackupOnboardingInfo {
|
||||||
|
backups: BackupContentExtended[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchBackupOnboardingInfo = async () =>
|
||||||
|
handleFetchPromise<BackupOnboardingConfig>(
|
||||||
|
fetch("/api/onboarding/backup/info")
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface RestoreOnboardingBackupParams {
|
||||||
|
backup_id: string;
|
||||||
|
agent_id: string;
|
||||||
|
password?: string;
|
||||||
|
restore_addons?: string[];
|
||||||
|
restore_database?: boolean;
|
||||||
|
restore_folders?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreOnboardingBackup = async (
|
||||||
|
params: RestoreOnboardingBackupParams
|
||||||
|
) =>
|
||||||
|
handleFetchPromise(
|
||||||
|
fetch("/api/onboarding/backup/restore", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const uploadOnboardingBackup = async (
|
||||||
|
file: File,
|
||||||
|
agentIds: string[]
|
||||||
|
): Promise<{ backup_id: string }> => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
agentIds.forEach((agentId) => {
|
||||||
|
params.append("agent_id", agentId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return handleFetchPromise(
|
||||||
|
fetch(`/api/onboarding/backup/upload?${params.toString()}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -181,3 +181,6 @@ export const updateCloudGoogleEntityConfig = (
|
|||||||
|
|
||||||
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
||||||
hass.callApi("POST", "cloud/google_actions/sync");
|
hass.callApi("POST", "cloud/google_actions/sync");
|
||||||
|
|
||||||
|
export const fetchSupportPackage = (hass: HomeAssistant) =>
|
||||||
|
hass.callApi<string>("GET", "cloud/support_package");
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export interface ConfigEntry {
|
|||||||
supports_remove_device: boolean;
|
supports_remove_device: boolean;
|
||||||
supports_unload: boolean;
|
supports_unload: boolean;
|
||||||
supports_reconfigure: boolean;
|
supports_reconfigure: boolean;
|
||||||
|
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
|
||||||
|
num_subentries: number;
|
||||||
pref_disable_new_entities: boolean;
|
pref_disable_new_entities: boolean;
|
||||||
pref_disable_polling: boolean;
|
pref_disable_polling: boolean;
|
||||||
disabled_by: "user" | null;
|
disabled_by: "user" | null;
|
||||||
@@ -27,6 +29,30 @@ export interface ConfigEntry {
|
|||||||
error_reason_translation_placeholders: Record<string, string> | null;
|
error_reason_translation_placeholders: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SubEntry {
|
||||||
|
subentry_id: string;
|
||||||
|
subentry_type: string;
|
||||||
|
title: string;
|
||||||
|
unique_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
|
||||||
|
hass.callWS<SubEntry[]>({
|
||||||
|
type: "config_entries/subentries/list",
|
||||||
|
entry_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteSubEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry_id: string,
|
||||||
|
subentry_id: string
|
||||||
|
) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "config_entries/subentries/delete",
|
||||||
|
entry_id,
|
||||||
|
subentry_id,
|
||||||
|
});
|
||||||
|
|
||||||
export type ConfigEntryMutableParams = Partial<
|
export type ConfigEntryMutableParams = Partial<
|
||||||
Pick<
|
Pick<
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import type { Connection } from "home-assistant-js-websocket";
|
|||||||
import type { HaFormSchema } from "../components/ha-form/types";
|
import type { HaFormSchema } from "../components/ha-form/types";
|
||||||
import type { ConfigEntry } from "./config_entries";
|
import type { ConfigEntry } from "./config_entries";
|
||||||
|
|
||||||
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
|
export type FlowType =
|
||||||
|
| "config_flow"
|
||||||
|
| "config_subentries_flow"
|
||||||
|
| "options_flow"
|
||||||
|
| "repair_flow";
|
||||||
|
|
||||||
export interface DataEntryFlowProgressedEvent {
|
export interface DataEntryFlowProgressedEvent {
|
||||||
type: "data_entry_flow_progressed";
|
type: "data_entry_flow_progressed";
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export {
|
|||||||
export interface DeviceRegistryEntry extends RegistryEntry {
|
export interface DeviceRegistryEntry extends RegistryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
config_entries: string[];
|
config_entries: string[];
|
||||||
|
config_entries_subentries: Record<string, (string | null)[]>;
|
||||||
connections: [string, string][];
|
connections: [string, string][];
|
||||||
identifiers: [string, string][];
|
identifiers: [string, string][];
|
||||||
manufacturer: string | null;
|
manufacturer: string | null;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface EntityRegistryEntry extends RegistryEntry {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
platform: string;
|
platform: string;
|
||||||
config_entry_id: string | null;
|
config_entry_id: string | null;
|
||||||
|
config_subentry_id: string | null;
|
||||||
device_id: string | null;
|
device_id: string | null;
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { atLeastVersion } from "../../common/config/version";
|
import { atLeastVersion } from "../../common/config/version";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { handleFetchPromise } from "../../util/hass-call-api";
|
|
||||||
import type { HassioResponse } from "./common";
|
import type { HassioResponse } from "./common";
|
||||||
import { hassioApiResultExtractor } from "./common";
|
import { hassioApiResultExtractor } from "./common";
|
||||||
|
|
||||||
@@ -82,10 +81,9 @@ export const fetchHassioBackups = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchHassioBackupInfo = async (
|
export const fetchHassioBackupInfo = async (
|
||||||
hass: HomeAssistant | undefined,
|
hass: HomeAssistant,
|
||||||
backup: string
|
backup: string
|
||||||
): Promise<HassioBackupDetail> => {
|
): Promise<HassioBackupDetail> => {
|
||||||
if (hass) {
|
|
||||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||||
return hass.callWS({
|
return hass.callWS({
|
||||||
type: "supervisor/api",
|
type: "supervisor/api",
|
||||||
@@ -103,15 +101,6 @@ export const fetchHassioBackupInfo = async (
|
|||||||
}/${backup}/info`
|
}/${backup}/info`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// When called from onboarding we don't have hass
|
|
||||||
return hassioApiResultExtractor(
|
|
||||||
await handleFetchPromise(
|
|
||||||
fetch(`/api/hassio/backups/${backup}/info`, {
|
|
||||||
method: "GET",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reloadHassioBackups = async (hass: HomeAssistant) => {
|
export const reloadHassioBackups = async (hass: HomeAssistant) => {
|
||||||
@@ -240,24 +229,15 @@ export const uploadBackup = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const restoreBackup = async (
|
export const restoreBackup = async (
|
||||||
hass: HomeAssistant | undefined,
|
hass: HomeAssistant,
|
||||||
type: HassioBackupDetail["type"],
|
type: HassioBackupDetail["type"],
|
||||||
backupSlug: string,
|
backupSlug: string,
|
||||||
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
|
||||||
useSnapshotUrl: boolean
|
useSnapshotUrl: boolean
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (hass) {
|
|
||||||
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
await hass.callApi<HassioResponse<{ job_id: string }>>(
|
||||||
"POST",
|
"POST",
|
||||||
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
|
||||||
backupDetails
|
backupDetails
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
await handleFetchPromise(
|
|
||||||
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(backupDetails),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
46
src/data/sub_config_flow.ts
Normal file
46
src/data/sub_config_flow.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import type { DataEntryFlowStep } from "./data_entry_flow";
|
||||||
|
|
||||||
|
const HEADERS = {
|
||||||
|
"HA-Frontend-Base": `${location.protocol}//${location.host}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSubConfigFlow = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
configEntryId: string,
|
||||||
|
subFlowType: string,
|
||||||
|
subentry_id?: string
|
||||||
|
) =>
|
||||||
|
hass.callApi<DataEntryFlowStep>(
|
||||||
|
"POST",
|
||||||
|
"config/config_entries/subentries/flow",
|
||||||
|
{
|
||||||
|
handler: [configEntryId, subFlowType],
|
||||||
|
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||||
|
subentry_id,
|
||||||
|
},
|
||||||
|
HEADERS
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||||
|
hass.callApi<DataEntryFlowStep>(
|
||||||
|
"GET",
|
||||||
|
`config/config_entries/subentries/flow/${flowId}`,
|
||||||
|
undefined,
|
||||||
|
HEADERS
|
||||||
|
);
|
||||||
|
|
||||||
|
export const handleSubConfigFlowStep = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
flowId: string,
|
||||||
|
data: Record<string, any>
|
||||||
|
) =>
|
||||||
|
hass.callApi<DataEntryFlowStep>(
|
||||||
|
"POST",
|
||||||
|
`config/config_entries/subentries/flow/${flowId}`,
|
||||||
|
data,
|
||||||
|
HEADERS
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteSubConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||||
|
hass.callApi("DELETE", `config/config_entries/subentries/flow/${flowId}`);
|
||||||
@@ -63,6 +63,7 @@ export type TranslationCategory =
|
|||||||
| "entity_component"
|
| "entity_component"
|
||||||
| "exceptions"
|
| "exceptions"
|
||||||
| "config"
|
| "config"
|
||||||
|
| "config_subentries"
|
||||||
| "config_panel"
|
| "config_panel"
|
||||||
| "options"
|
| "options"
|
||||||
| "device_automation"
|
| "device_automation"
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { Connection } from "home-assistant-js-websocket";
|
import type { Connection } from "home-assistant-js-websocket";
|
||||||
import { createCollection } from "home-assistant-js-websocket";
|
import { createCollection } from "home-assistant-js-websocket";
|
||||||
|
import type { HomeAssistant, ThemeSettings } from "../types";
|
||||||
|
import {
|
||||||
|
fetchFrontendUserData,
|
||||||
|
saveFrontendUserData,
|
||||||
|
subscribeFrontendUserData,
|
||||||
|
} from "./frontend";
|
||||||
|
|
||||||
export interface ThemeVars {
|
export interface ThemeVars {
|
||||||
// Incomplete
|
// Incomplete
|
||||||
@@ -50,3 +56,16 @@ export const subscribeThemes = (
|
|||||||
conn,
|
conn,
|
||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SELECTED_THEME_KEY = "selectedTheme";
|
||||||
|
|
||||||
|
export const saveSelectedTheme = (hass: HomeAssistant, data?: ThemeSettings) =>
|
||||||
|
saveFrontendUserData(hass.connection, SELECTED_THEME_KEY, data);
|
||||||
|
|
||||||
|
export const subscribeSelectedTheme = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
callback: (selectedTheme?: ThemeSettings | null) => void
|
||||||
|
) => subscribeFrontendUserData(hass.connection, SELECTED_THEME_KEY, callback);
|
||||||
|
|
||||||
|
export const fetchSelectedTheme = (hass: HomeAssistant) =>
|
||||||
|
fetchFrontendUserData(hass.connection, SELECTED_THEME_KEY);
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.navigateToResult=${this._params
|
||||||
|
.navigateToResult}
|
||||||
></step-flow-create-entry>
|
></step-flow-create-entry>
|
||||||
`}
|
`}
|
||||||
`}
|
`}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
|
|||||||
(await this._unsub)();
|
(await this._unsub)();
|
||||||
this._unsub = undefined;
|
this._unsub = undefined;
|
||||||
}
|
}
|
||||||
if (this.flowType === "repair_flow") {
|
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
|
|||||||
(await this._unsub)();
|
(await this._unsub)();
|
||||||
this._unsub = undefined;
|
this._unsub = undefined;
|
||||||
}
|
}
|
||||||
if (this.flowType === "repair_flow") {
|
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
|
|||||||
|
|
||||||
export const showConfigFlowDialog = (
|
export const showConfigFlowDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
|
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||||
|
entryId?: string;
|
||||||
|
}
|
||||||
): void =>
|
): void =>
|
||||||
showFlowDialog(element, dialogParams, {
|
showFlowDialog(element, dialogParams, {
|
||||||
flowType: "config_flow",
|
flowType: "config_flow",
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
|
|||||||
}) => void;
|
}) => void;
|
||||||
flowConfig: FlowConfig;
|
flowConfig: FlowConfig;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
entryId?: string;
|
|
||||||
dialogParentElement?: HTMLElement;
|
dialogParentElement?: HTMLElement;
|
||||||
|
navigateToResult?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");
|
||||||
|
|||||||
275
src/dialogs/config-flow/show-dialog-sub-config-flow.ts
Normal file
275
src/dialogs/config-flow/show-dialog-sub-config-flow.ts
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import { html } from "lit";
|
||||||
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
|
import { domainToName } from "../../data/integration";
|
||||||
|
import {
|
||||||
|
createSubConfigFlow,
|
||||||
|
deleteSubConfigFlow,
|
||||||
|
fetchSubConfigFlow,
|
||||||
|
handleSubConfigFlowStep,
|
||||||
|
} from "../../data/sub_config_flow";
|
||||||
|
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||||
|
import {
|
||||||
|
loadDataEntryFlowDialog,
|
||||||
|
showFlowDialog,
|
||||||
|
} from "./show-dialog-data-entry-flow";
|
||||||
|
|
||||||
|
export const loadSubConfigFlowDialog = loadDataEntryFlowDialog;
|
||||||
|
|
||||||
|
export const showSubConfigFlowDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
configEntry: ConfigEntry,
|
||||||
|
flowType: string,
|
||||||
|
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
|
||||||
|
subEntryId?: string;
|
||||||
|
}
|
||||||
|
): void =>
|
||||||
|
showFlowDialog(element, dialogParams, {
|
||||||
|
flowType: "config_subentries_flow",
|
||||||
|
showDevices: true,
|
||||||
|
createFlow: async (hass, handler) => {
|
||||||
|
const [step] = await Promise.all([
|
||||||
|
createSubConfigFlow(hass, handler, flowType, dialogParams.subEntryId),
|
||||||
|
hass.loadFragmentTranslation("config"),
|
||||||
|
hass.loadBackendTranslation("config_subentries", configEntry.domain),
|
||||||
|
hass.loadBackendTranslation("selector", configEntry.domain),
|
||||||
|
// Used as fallback if no header defined for step
|
||||||
|
hass.loadBackendTranslation("title", configEntry.domain),
|
||||||
|
]);
|
||||||
|
return step;
|
||||||
|
},
|
||||||
|
fetchFlow: async (hass, flowId) => {
|
||||||
|
const step = await fetchSubConfigFlow(hass, flowId);
|
||||||
|
await hass.loadFragmentTranslation("config");
|
||||||
|
await hass.loadBackendTranslation(
|
||||||
|
"config_subentries",
|
||||||
|
configEntry.domain
|
||||||
|
);
|
||||||
|
await hass.loadBackendTranslation("selector", configEntry.domain);
|
||||||
|
return step;
|
||||||
|
},
|
||||||
|
handleFlowStep: handleSubConfigFlowStep,
|
||||||
|
deleteFlow: deleteSubConfigFlow,
|
||||||
|
|
||||||
|
renderAbortDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
return description
|
||||||
|
? html`
|
||||||
|
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||||
|
`
|
||||||
|
: step.reason;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepHeader(hass, step) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||||
|
step.description_placeholders
|
||||||
|
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
return description
|
||||||
|
? html`
|
||||||
|
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||||
|
if (field.type === "expandable") {
|
||||||
|
return hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data.${field.name}`
|
||||||
|
) || field.name
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||||
|
if (field.type === "expandable") {
|
||||||
|
return hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.description`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
return description
|
||||||
|
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||||
|
: "";
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepFieldError(hass, step, error) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${step.translation_domain || step.translation_domain || configEntry.domain}.config_subentries.${flowType}.error.${error}`,
|
||||||
|
step.description_placeholders
|
||||||
|
) || error
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepFieldLocalizeValue(hass, _step, key) {
|
||||||
|
return hass.localize(`component.${configEntry.domain}.selector.${key}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormStepSubmitButton(hass, step) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.submit`
|
||||||
|
) ||
|
||||||
|
hass.localize(
|
||||||
|
`ui.panel.config.integrations.config_flow.${
|
||||||
|
step.last_step === false ? "next" : "submit"
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderExternalStepHeader(hass, step) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||||
|
) ||
|
||||||
|
hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderExternalStepDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<p>
|
||||||
|
${hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.external_step.description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${description
|
||||||
|
? html`
|
||||||
|
<ha-markdown
|
||||||
|
allowsvg
|
||||||
|
breaks
|
||||||
|
.content=${description}
|
||||||
|
></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCreateEntryDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.create_entry.${
|
||||||
|
step.description || "default"
|
||||||
|
}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${description
|
||||||
|
? html`
|
||||||
|
<ha-markdown
|
||||||
|
allowsvg
|
||||||
|
breaks
|
||||||
|
.content=${description}
|
||||||
|
></ha-markdown>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<p>
|
||||||
|
${hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.created_config",
|
||||||
|
{ name: step.title }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormProgressHeader(hass, step) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`
|
||||||
|
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderShowFormProgressDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.progress.${step.progress_action}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
return description
|
||||||
|
? html`
|
||||||
|
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMenuHeader(hass, step) {
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.title`,
|
||||||
|
step.description_placeholders
|
||||||
|
) || hass.localize(`component.${configEntry.domain}.title`)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMenuDescription(hass, step) {
|
||||||
|
const description = hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.description`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
return description
|
||||||
|
? html`
|
||||||
|
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMenuOption(hass, step, option) {
|
||||||
|
return hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_options.${option}`,
|
||||||
|
step.description_placeholders
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLoadingDescription(hass, reason, handler, step) {
|
||||||
|
if (reason !== "loading_flow" && reason !== "loading_step") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const domain = step?.handler || handler;
|
||||||
|
return hass.localize(
|
||||||
|
`ui.panel.config.integrations.config_flow.loading.${reason}`,
|
||||||
|
{
|
||||||
|
integration: domain
|
||||||
|
? domainToName(hass.localize, domain)
|
||||||
|
: // when we are continuing a config flow, we only know the ID and not the domain
|
||||||
|
hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_flow.loading.fallback_title"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -60,6 +60,7 @@ class StepFlowAbort extends LitElement {
|
|||||||
dialogClosedCallback: this.params.dialogClosedCallback,
|
dialogClosedCallback: this.params.dialogClosedCallback,
|
||||||
startFlowHandler: this.domain,
|
startFlowHandler: this.domain,
|
||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
|
navigateToResult: this.params.navigateToResult,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { showAlertDialog } from "../generic/show-dialog-box";
|
|||||||
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
||||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||||
import { configFlowContentStyles } from "./styles";
|
import { configFlowContentStyles } from "./styles";
|
||||||
|
import { navigate } from "../../common/navigate";
|
||||||
|
|
||||||
@customElement("step-flow-create-entry")
|
@customElement("step-flow-create-entry")
|
||||||
class StepFlowCreateEntry extends LitElement {
|
class StepFlowCreateEntry extends LitElement {
|
||||||
@@ -28,6 +29,8 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
|
||||||
|
|
||||||
|
navigateToResult = false;
|
||||||
|
|
||||||
private _devices = memoizeOne(
|
private _devices = memoizeOne(
|
||||||
(
|
(
|
||||||
showDevices: boolean,
|
showDevices: boolean,
|
||||||
@@ -65,7 +68,8 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
devices.length !== 1 ||
|
devices.length !== 1 ||
|
||||||
devices[0].primary_config_entry !== this.step.result?.entry_id
|
devices[0].primary_config_entry !== this.step.result?.entry_id ||
|
||||||
|
this.step.result.domain === "voip"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,6 +155,11 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
|
|
||||||
private _flowDone(): void {
|
private _flowDone(): void {
|
||||||
fireEvent(this, "flow-update", { step: undefined });
|
fireEvent(this, "flow-update", { step: undefined });
|
||||||
|
if (this.step.result && this.navigateToResult) {
|
||||||
|
navigate(
|
||||||
|
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _areaPicked(ev: CustomEvent) {
|
private async _areaPicked(ev: CustomEvent) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
|
|||||||
"switch",
|
"switch",
|
||||||
"valve",
|
"valve",
|
||||||
"water_heater",
|
"water_heater",
|
||||||
|
"weather",
|
||||||
];
|
];
|
||||||
/** Domains with full height more info dialog */
|
/** Domains with full height more info dialog */
|
||||||
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];
|
||||||
|
|||||||
@@ -448,6 +448,10 @@ class MoreInfoUpdate extends LitElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: -16px;
|
margin-bottom: -16px;
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
|
--md-sys-color-surface: var(
|
||||||
|
--ha-dialog-surface-background,
|
||||||
|
var(--mdc-theme-surface, #fff)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-md-list-item {
|
ha-md-list-item {
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import "@material/mwc-tab";
|
import "@material/mwc-tab";
|
||||||
import "@material/mwc-tab-bar";
|
import "@material/mwc-tab-bar";
|
||||||
import {
|
import { mdiEye, mdiGauge, mdiWaterPercent, mdiWeatherWindy } from "@mdi/js";
|
||||||
mdiEye,
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
mdiGauge,
|
|
||||||
mdiThermometer,
|
|
||||||
mdiWaterPercent,
|
|
||||||
mdiWeatherWindy,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
|
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
|
||||||
import { formatTimeWeekday } from "../../../common/datetime/format_time";
|
import { formatTime } from "../../../common/datetime/format_time";
|
||||||
|
import { formatNumber } from "../../../common/number/format_number";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type {
|
import type {
|
||||||
ForecastEvent,
|
ForecastEvent,
|
||||||
@@ -23,11 +18,16 @@ import {
|
|||||||
getDefaultForecastType,
|
getDefaultForecastType,
|
||||||
getForecast,
|
getForecast,
|
||||||
getSupportedForecastTypes,
|
getSupportedForecastTypes,
|
||||||
|
getSecondaryWeatherAttribute,
|
||||||
|
getWeatherStateIcon,
|
||||||
|
getWeatherUnit,
|
||||||
getWind,
|
getWind,
|
||||||
subscribeForecast,
|
subscribeForecast,
|
||||||
weatherIcons,
|
weatherSVGStyles,
|
||||||
} from "../../../data/weather";
|
} from "../../../data/weather";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import "../../../components/ha-relative-time";
|
||||||
|
import "../../../components/ha-state-icon";
|
||||||
|
|
||||||
@customElement("more-info-weather")
|
@customElement("more-info-weather")
|
||||||
class MoreInfoWeather extends LitElement {
|
class MoreInfoWeather extends LitElement {
|
||||||
@@ -137,23 +137,90 @@ class MoreInfoWeather extends LitElement {
|
|||||||
const hourly = forecastData?.type === "hourly";
|
const hourly = forecastData?.type === "hourly";
|
||||||
const dayNight = forecastData?.type === "twice_daily";
|
const dayNight = forecastData?.type === "twice_daily";
|
||||||
|
|
||||||
|
const weatherStateIcon = getWeatherStateIcon(this.stateObj.state, this);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this._showValue(this.stateObj.attributes.temperature)
|
<div class="content">
|
||||||
? html`
|
<div class="icon-image">
|
||||||
<div class="flex">
|
${weatherStateIcon ||
|
||||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
html`
|
||||||
<div class="main">
|
<ha-state-icon
|
||||||
${this.hass.localize("ui.card.weather.attributes.temperature")}
|
class="weather-icon"
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></ha-state-icon>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div class="name-state">
|
||||||
|
<div class="state">
|
||||||
|
${this.hass.formatEntityState(this.stateObj)}
|
||||||
|
</div>
|
||||||
|
<div class="time-ago">
|
||||||
|
<ha-relative-time
|
||||||
|
id="last_changed"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.datetime=${this.stateObj.last_changed}
|
||||||
|
capitalize
|
||||||
|
></ha-relative-time>
|
||||||
|
<simple-tooltip animation-delay="0" for="last_changed">
|
||||||
<div>
|
<div>
|
||||||
${this.hass.formatEntityAttributeValue(
|
<div class="row">
|
||||||
|
<span class="column-name">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.last_changed"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<ha-relative-time
|
||||||
|
.hass=${this.hass}
|
||||||
|
.datetime=${this.stateObj.last_changed}
|
||||||
|
capitalize
|
||||||
|
></ha-relative-time>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.more_info_control.last_updated"
|
||||||
|
)}:
|
||||||
|
</span>
|
||||||
|
<ha-relative-time
|
||||||
|
.hass=${this.hass}
|
||||||
|
.datetime=${this.stateObj.last_updated}
|
||||||
|
capitalize
|
||||||
|
></ha-relative-time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</simple-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="temp-attribute">
|
||||||
|
<div class="temp">
|
||||||
|
${this.stateObj.attributes.temperature !== undefined &&
|
||||||
|
this.stateObj.attributes.temperature !== null
|
||||||
|
? html`
|
||||||
|
${formatNumber(
|
||||||
|
this.stateObj.attributes.temperature,
|
||||||
|
this.hass.locale
|
||||||
|
)} <span
|
||||||
|
>${getWeatherUnit(
|
||||||
|
this.hass.config,
|
||||||
this.stateObj,
|
this.stateObj,
|
||||||
"temperature"
|
"temperature"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div class="attribute">
|
||||||
|
${getSecondaryWeatherAttribute(
|
||||||
|
this.hass,
|
||||||
|
this.stateObj,
|
||||||
|
forecast!
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
</div>
|
||||||
: ""}
|
</div>
|
||||||
${this._showValue(this.stateObj.attributes.pressure)
|
${this._showValue(this.stateObj.attributes.pressure)
|
||||||
? html`
|
? html`
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -169,7 +236,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._showValue(this.stateObj.attributes.humidity)
|
${this._showValue(this.stateObj.attributes.humidity)
|
||||||
? html`
|
? html`
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -185,7 +252,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._showValue(this.stateObj.attributes.wind_speed)
|
${this._showValue(this.stateObj.attributes.wind_speed)
|
||||||
? html`
|
? html`
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -203,7 +270,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._showValue(this.stateObj.attributes.visibility)
|
${this._showValue(this.stateObj.attributes.visibility)
|
||||||
? html`
|
? html`
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -219,7 +286,7 @@ class MoreInfoWeather extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${forecast
|
${forecast
|
||||||
? html`
|
? html`
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -242,76 +309,90 @@ class MoreInfoWeather extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</mwc-tab-bar>`
|
</mwc-tab-bar>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
<div class="forecast">
|
||||||
${forecast.map((item) =>
|
${forecast.map((item) =>
|
||||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
this._showValue(item.templow) ||
|
||||||
? html`<div class="flex">
|
this._showValue(item.temperature)
|
||||||
${item.condition
|
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<div>
|
||||||
.path=${weatherIcons[item.condition]}
|
<div>
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div class="main">
|
|
||||||
${dayNight
|
${dayNight
|
||||||
? html`
|
? html`
|
||||||
${formatDateWeekdayDay(
|
${formatDateWeekdayShort(
|
||||||
new Date(item.datetime),
|
new Date(item.datetime),
|
||||||
this.hass!.locale,
|
this.hass!.locale,
|
||||||
this.hass!.config
|
this.hass!.config
|
||||||
)}
|
)}
|
||||||
(${item.is_daytime !== false
|
<div class="daynight">
|
||||||
|
${item.is_daytime !== false
|
||||||
? this.hass!.localize("ui.card.weather.day")
|
? this.hass!.localize("ui.card.weather.day")
|
||||||
: this.hass!.localize("ui.card.weather.night")})
|
: this.hass!.localize(
|
||||||
|
"ui.card.weather.night"
|
||||||
|
)}<br />
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
: hourly
|
: hourly
|
||||||
? html`
|
? html`
|
||||||
${formatTimeWeekday(
|
${formatTime(
|
||||||
new Date(item.datetime),
|
new Date(item.datetime),
|
||||||
this.hass!.locale,
|
this.hass!.locale,
|
||||||
this.hass!.config
|
this.hass!.config
|
||||||
)}
|
)}
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
${formatDateWeekdayDay(
|
${formatDateWeekdayShort(
|
||||||
new Date(item.datetime),
|
new Date(item.datetime),
|
||||||
this.hass!.locale,
|
this.hass!.locale,
|
||||||
this.hass!.config
|
this.hass!.config
|
||||||
)}
|
)}
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
<div class="templow">
|
${this._showValue(item.condition)
|
||||||
${this._showValue(item.templow)
|
? html`
|
||||||
? this.hass.formatEntityAttributeValue(
|
<div class="forecast-image-icon">
|
||||||
this.stateObj!,
|
${getWeatherStateIcon(
|
||||||
"templow",
|
item.condition!,
|
||||||
item.templow
|
this,
|
||||||
|
!(
|
||||||
|
item.is_daytime ||
|
||||||
|
item.is_daytime === undefined
|
||||||
)
|
)
|
||||||
: hourly
|
)}
|
||||||
? ""
|
|
||||||
: "—"}
|
|
||||||
</div>
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
<div class="temp">
|
<div class="temp">
|
||||||
${this._showValue(item.temperature)
|
${this._showValue(item.temperature)
|
||||||
? this.hass.formatEntityAttributeValue(
|
? html`${formatNumber(
|
||||||
this.stateObj!,
|
item.temperature,
|
||||||
"temperature",
|
this.hass!.locale
|
||||||
item.temperature
|
)}°`
|
||||||
)
|
|
||||||
: "—"}
|
: "—"}
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
<div class="templow">
|
||||||
: ""
|
${this._showValue(item.templow)
|
||||||
)}
|
? html`${formatNumber(
|
||||||
|
item.templow!,
|
||||||
|
this.hass!.locale
|
||||||
|
)}°`
|
||||||
|
: hourly
|
||||||
|
? nothing
|
||||||
|
: "—"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
${this.stateObj.attributes.attribution
|
${this.stateObj.attributes.attribution
|
||||||
? html`
|
? html`
|
||||||
<div class="attribution">
|
<div class="attribution">
|
||||||
${this.stateObj.attributes.attribution}
|
${this.stateObj.attributes.attribution}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +402,10 @@ class MoreInfoWeather extends LitElement {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
weatherSVGStyles,
|
||||||
|
css`
|
||||||
ha-svg-icon {
|
ha-svg-icon {
|
||||||
color: var(--paper-item-icon-color);
|
color: var(--paper-item-icon-color);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -354,23 +438,150 @@ class MoreInfoWeather extends LitElement {
|
|||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.temp,
|
.attribution {
|
||||||
.templow {
|
text-align: center;
|
||||||
min-width: 48px;
|
margin-top: 16px;
|
||||||
text-align: right;
|
}
|
||||||
|
|
||||||
|
.time-ago,
|
||||||
|
.attribute {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribution,
|
||||||
|
.templow,
|
||||||
|
.daynight,
|
||||||
|
.attribute,
|
||||||
|
.time-ago {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 64px;
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-image > * {
|
||||||
|
flex: 0 0 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
--mdc-icon-size: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-attribute {
|
||||||
|
text-align: var(--float-end);
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-attribute .temp {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 24px;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.templow {
|
.temp-attribute .temp span {
|
||||||
margin: 0 16px;
|
position: absolute;
|
||||||
color: var(--secondary-text-color);
|
font-size: 24px;
|
||||||
|
top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribution {
|
.state,
|
||||||
color: var(--secondary-text-color);
|
.temp-attribute .temp {
|
||||||
text-align: center;
|
font-size: 28px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-state {
|
||||||
|
overflow: hidden;
|
||||||
|
padding-right: 12px;
|
||||||
|
padding-inline-end: 12px;
|
||||||
|
padding-inline-start: initial;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 16px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
black 5%,
|
||||||
|
black 94%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast > div {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast .icon,
|
||||||
|
.forecast .temp {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast .temp {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-image-icon {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-image-icon > * {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
--mdc-icon-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecast-icon {
|
||||||
|
--mdc-icon-size: 40px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
private _showValue(item: number | string | undefined): boolean {
|
private _showValue(item: number | string | undefined): boolean {
|
||||||
return typeof item !== "undefined" && item !== null;
|
return typeof item !== "undefined" && item !== null;
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
|
|
||||||
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
|
@state() private _assistConfiguration?: AssistSatelliteConfiguration;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
private _previousSteps: STEP[] = [];
|
private _previousSteps: STEP[] = [];
|
||||||
|
|
||||||
private _nextStep?: STEP;
|
private _nextStep?: STEP;
|
||||||
@@ -165,10 +167,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
"update"
|
"update"
|
||||||
)}
|
)}
|
||||||
></ha-voice-assistant-setup-step-update>`
|
></ha-voice-assistant-setup-step-update>`
|
||||||
|
: this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: assistEntityState?.state === UNAVAILABLE
|
: assistEntityState?.state === UNAVAILABLE
|
||||||
? this.hass.localize(
|
? html`<ha-alert alert-type="error"
|
||||||
|
>${this.hass.localize(
|
||||||
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
|
"ui.panel.config.voice_assistants.satellite_wizard.not_available"
|
||||||
)
|
)}</ha-alert
|
||||||
|
>`
|
||||||
: this._step === STEP.CHECK
|
: this._step === STEP.CHECK
|
||||||
? html`<ha-voice-assistant-setup-step-check
|
? html`<ha-voice-assistant-setup-step-check
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -229,6 +235,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchAssistConfiguration() {
|
private async _fetchAssistConfiguration() {
|
||||||
|
try {
|
||||||
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
|
this._assistConfiguration = await fetchAssistSatelliteConfiguration(
|
||||||
this.hass,
|
this.hass,
|
||||||
this._findDomainEntityId(
|
this._findDomainEntityId(
|
||||||
@@ -237,7 +244,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
"assist_satellite"
|
"assist_satellite"
|
||||||
)!
|
)!
|
||||||
);
|
);
|
||||||
return this._assistConfiguration;
|
} catch (err: any) {
|
||||||
|
this._error = err.message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _goToPreviousStep() {
|
private _goToPreviousStep() {
|
||||||
@@ -293,6 +302,10 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
|||||||
.skip-btn {
|
.skip-btn {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
ha-alert {
|
||||||
|
margin: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
protected override willUpdate(changedProperties: PropertyValues) {
|
protected override willUpdate(changedProperties: PropertyValues) {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has("assistConfiguration")) {
|
||||||
|
if (
|
||||||
|
this.assistConfiguration &&
|
||||||
|
!this.assistConfiguration.available_wake_words.length
|
||||||
|
) {
|
||||||
|
this._nextStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProperties.has("assistEntityId")) {
|
if (changedProperties.has("assistEntityId")) {
|
||||||
this._detected = false;
|
this._detected = false;
|
||||||
this._muteSwitchEntity = this.deviceEntities?.find(
|
this._muteSwitchEntity = this.deviceEntities?.find(
|
||||||
@@ -135,13 +144,16 @@ export class HaVoiceAssistantSetupStepWakeWord extends LitElement {
|
|||||||
>`
|
>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer centered">
|
${this.assistConfiguration &&
|
||||||
|
this.assistConfiguration.available_wake_words.length > 1
|
||||||
|
? html`<div class="footer centered">
|
||||||
<ha-button @click=${this._changeWakeWord}
|
<ha-button @click=${this._changeWakeWord}
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
|
"ui.panel.config.voice_assistants.satellite_wizard.wake_word.change_wake_word"
|
||||||
)}</ha-button
|
)}</ha-button
|
||||||
>
|
>
|
||||||
</div>`;
|
</div>`
|
||||||
|
: nothing}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _listenWakeWord() {
|
private async _listenWakeWord() {
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
<meta name="color-scheme" content="dark light" />
|
<meta name="color-scheme" content="dark light" />
|
||||||
<%= renderTemplate("_style_base.html.template") %>
|
<%= renderTemplate("_style_base.html.template") %>
|
||||||
<style>
|
<style>
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--primary-background-color, #fafafa);
|
background-color: var(--primary-background-color, #fafafa);
|
||||||
color: var(--primary-text-color, #212121);
|
color: var(--primary-text-color, #212121);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
|||||||
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
@property({ attribute: false }) public localize: LocalizeFunc = empty;
|
||||||
|
|
||||||
// Use browser language setup before login.
|
// Use browser language setup before login.
|
||||||
@property() public language?: string = getLocalLanguage();
|
@property() public language: string = getLocalLanguage();
|
||||||
|
|
||||||
@property() public translationFragment?: string;
|
@property() public translationFragment?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import "./onboarding-analytics";
|
|||||||
import "./onboarding-create-user";
|
import "./onboarding-create-user";
|
||||||
import "./onboarding-loading";
|
import "./onboarding-loading";
|
||||||
import "./onboarding-welcome";
|
import "./onboarding-welcome";
|
||||||
|
import "./onboarding-restore-backup";
|
||||||
import "./onboarding-welcome-links";
|
import "./onboarding-welcome-links";
|
||||||
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
import { makeDialogManager } from "../dialogs/make-dialog-manager";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
@@ -157,8 +158,9 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
private _renderStep() {
|
private _renderStep() {
|
||||||
if (this._restoring) {
|
if (this._restoring) {
|
||||||
return html`<onboarding-restore-backup
|
return html`<onboarding-restore-backup
|
||||||
.hass=${this.hass}
|
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
|
.supervisor=${this._supervisor ?? false}
|
||||||
|
.language=${this.language}
|
||||||
>
|
>
|
||||||
</onboarding-restore-backup>`;
|
</onboarding-restore-backup>`;
|
||||||
}
|
}
|
||||||
@@ -166,8 +168,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
if (this._init) {
|
if (this._init) {
|
||||||
return html`<onboarding-welcome
|
return html`<onboarding-welcome
|
||||||
.localize=${this.localize}
|
.localize=${this.localize}
|
||||||
.language=${this.language}
|
|
||||||
.supervisor=${this._supervisor}
|
|
||||||
></onboarding-welcome>`;
|
></onboarding-welcome>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedProps.has("language")) {
|
if (changedProps.has("language")) {
|
||||||
document.querySelector("html")!.setAttribute("lang", this.language!);
|
document.querySelector("html")!.setAttribute("lang", this.language);
|
||||||
}
|
}
|
||||||
if (changedProps.has("hass")) {
|
if (changedProps.has("hass")) {
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
@@ -272,10 +272,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
"Home Assistant OS",
|
"Home Assistant OS",
|
||||||
"Home Assistant Supervised",
|
"Home Assistant Supervised",
|
||||||
].includes(response.installation_type);
|
].includes(response.installation_type);
|
||||||
if (this._supervisor) {
|
|
||||||
// Only load if we have supervisor
|
|
||||||
import("./onboarding-restore-backup");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(
|
console.error(
|
||||||
@@ -454,7 +450,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
|
|||||||
subscribeOne(conn, subscribeUser),
|
subscribeOne(conn, subscribeUser),
|
||||||
]);
|
]);
|
||||||
this.initializeHass(auth, conn);
|
this.initializeHass(auth, conn);
|
||||||
if (this.language && this.language !== this.hass!.language) {
|
if (this.language !== this.hass!.language) {
|
||||||
this._updateHass({
|
this._updateHass({
|
||||||
locale: { ...this.hass!.locale, language: this.language },
|
locale: { ...this.hass!.locale, language: this.language },
|
||||||
language: this.language,
|
language: this.language,
|
||||||
|
|||||||
@@ -1,137 +1,337 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
|
import "./restore-backup/onboarding-restore-backup-upload";
|
||||||
import "../../hassio/src/components/hassio-upload-backup";
|
import "./restore-backup/onboarding-restore-backup-details";
|
||||||
|
import "./restore-backup/onboarding-restore-backup-restore";
|
||||||
|
import "./restore-backup/onboarding-restore-backup-status";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import "../components/ha-ansi-to-html";
|
|
||||||
import "../components/ha-card";
|
import "../components/ha-card";
|
||||||
|
import "../components/ha-icon-button-arrow-prev";
|
||||||
|
import "../components/ha-circular-progress";
|
||||||
import "../components/ha-alert";
|
import "../components/ha-alert";
|
||||||
import "../components/ha-button";
|
|
||||||
import { fetchInstallationType } from "../data/onboarding";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./onboarding-loading";
|
import "./onboarding-loading";
|
||||||
import { onBoardingStyles } from "./styles";
|
|
||||||
import { removeSearchParam } from "../common/url/search-params";
|
import { removeSearchParam } from "../common/url/search-params";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
|
import { onBoardingStyles } from "./styles";
|
||||||
|
import {
|
||||||
|
fetchBackupOnboardingInfo,
|
||||||
|
type BackupOnboardingConfig,
|
||||||
|
type BackupOnboardingInfo,
|
||||||
|
} from "../data/backup_onboarding";
|
||||||
|
import type { BackupContentExtended, BackupData } from "../data/backup";
|
||||||
|
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
|
import { storage } from "../common/decorators/storage";
|
||||||
|
|
||||||
|
const STATUS_INTERVAL_IN_MS = 5000;
|
||||||
|
|
||||||
@customElement("onboarding-restore-backup")
|
@customElement("onboarding-restore-backup")
|
||||||
class OnboardingRestoreBackup extends LitElement {
|
class OnboardingRestoreBackup extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
@property() public language!: string;
|
@property() public language!: string;
|
||||||
|
|
||||||
@state() private _restoring = false;
|
@property({ type: Boolean }) public supervisor = false;
|
||||||
|
|
||||||
@state() private _backupSlug?: string;
|
@state() private _view:
|
||||||
|
| "loading"
|
||||||
|
| "upload"
|
||||||
|
| "select_data"
|
||||||
|
| "confirm_restore"
|
||||||
|
| "status" = "loading";
|
||||||
|
|
||||||
|
@state() private _backup?: BackupContentExtended;
|
||||||
|
|
||||||
|
@state() private _backupInfo?: BackupOnboardingInfo;
|
||||||
|
|
||||||
|
@state() private _selectedData?: BackupData;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _failed?: boolean;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "onboarding-restore-backup-backup-id",
|
||||||
|
})
|
||||||
|
private _backupId?: string;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "onboarding-restore-running",
|
||||||
|
})
|
||||||
|
private _restoreRunning?: boolean;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
${this._restoring
|
${
|
||||||
? html`<h1>
|
this._view !== "status" || this._failed
|
||||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
? html`<ha-icon-button-arrow-prev
|
||||||
</h1>
|
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
|
||||||
<ha-alert alert-type="info">
|
@click=${this._back}
|
||||||
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
|
></ha-icon-button-arrow-prev>`
|
||||||
</ha-alert>
|
: nothing
|
||||||
<onboarding-loading></onboarding-loading>`
|
}
|
||||||
: html` <h1>
|
</ha-icon-button>
|
||||||
${this.localize("ui.panel.page-onboarding.restore.header")}
|
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
|
||||||
</h1>
|
${
|
||||||
<hassio-upload-backup
|
this._error || (this._failed && this._view !== "status")
|
||||||
@backup-uploaded=${this._backupUploaded}
|
? html`<ha-alert
|
||||||
@backup-cleared=${this._backupCleared}
|
alert-type="error"
|
||||||
.hass=${this.hass}
|
.title=${this._failed && this._view !== "status"
|
||||||
.localize=${this.localize}
|
? this.localize("ui.panel.page-onboarding.restore.failed")
|
||||||
></hassio-upload-backup>`}
|
: ""}
|
||||||
<div class="footer">
|
|
||||||
<ha-button @click=${this._back} .disabled=${this._restoring}>
|
|
||||||
${this.localize("ui.panel.page-onboarding.back")}
|
|
||||||
</ha-button>
|
|
||||||
${this._backupSlug
|
|
||||||
? html`<ha-button
|
|
||||||
@click=${this._showBackupDialog}
|
|
||||||
.disabled=${this._restoring}
|
|
||||||
>
|
>
|
||||||
${this.localize("ui.panel.page-onboarding.restore.restore")}
|
${this._failed && this._view !== "status"
|
||||||
</ha-button>`
|
? this.localize(
|
||||||
: nothing}
|
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
|
||||||
</div>
|
)
|
||||||
|
: this._error}
|
||||||
|
</ha-alert>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
this._view === "loading"
|
||||||
|
? html`<div class="loading">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
</div>`
|
||||||
|
: this._view === "upload"
|
||||||
|
? html`
|
||||||
|
<onboarding-restore-backup-upload
|
||||||
|
.supervisor=${this.supervisor}
|
||||||
|
.localize=${this.localize}
|
||||||
|
@backup-uploaded=${this._backupUploaded}
|
||||||
|
></onboarding-restore-backup-upload>
|
||||||
|
`
|
||||||
|
: this._view === "select_data"
|
||||||
|
? html`<onboarding-restore-backup-details
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backup=${this._backup!}
|
||||||
|
@backup-restore=${this._restore}
|
||||||
|
></onboarding-restore-backup-details>`
|
||||||
|
: this._view === "confirm_restore"
|
||||||
|
? html`<onboarding-restore-backup-restore
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backup=${this._backup!}
|
||||||
|
.supervisor=${this.supervisor}
|
||||||
|
.selectedData=${this._selectedData!}
|
||||||
|
@restore-started=${this._restoreStarted}
|
||||||
|
></onboarding-restore-backup-restore>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
this._view === "status" && this._backupInfo
|
||||||
|
? html`<onboarding-restore-backup-status
|
||||||
|
.localize=${this.localize}
|
||||||
|
.backupInfo=${this._backupInfo}
|
||||||
|
@show-backup-upload=${this._reupload}
|
||||||
|
></onboarding-restore-backup-status>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
${
|
||||||
|
["select_data", "confirm_restore"].includes(this._view) && this._backup
|
||||||
|
? html`<div class="backup-summary-wrapper">
|
||||||
|
<ha-backup-details-summary
|
||||||
|
translation-key-panel="page-onboarding.restore"
|
||||||
|
show-upload-another
|
||||||
|
.backup=${this._backup}
|
||||||
|
.localize=${this.localize}
|
||||||
|
@show-backup-upload=${this._reupload}
|
||||||
|
.isHassio=${this.supervisor}
|
||||||
|
></ha-backup-details-summary>
|
||||||
|
</div>`
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _back(): void {
|
|
||||||
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _backupUploaded(ev) {
|
|
||||||
const backup = ev.detail.backup;
|
|
||||||
this._backupSlug = backup.slug;
|
|
||||||
this._showBackupDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _backupCleared() {
|
|
||||||
this._backupSlug = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
|
this._loadBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkRestoreStatus(): Promise<void> {
|
private async _loadBackupInfo() {
|
||||||
if (this._restoring) {
|
let onboardingInfo: BackupOnboardingConfig;
|
||||||
try {
|
try {
|
||||||
await fetchInstallationType();
|
onboardingInfo = await fetchBackupOnboardingInfo();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (this._restoreRunning) {
|
||||||
if (
|
if (
|
||||||
(err as Error).message === "unauthorized" ||
|
err.error === "Request error" ||
|
||||||
(err as Error).message === "not_found"
|
// core can restart but haven't loaded the backup integration yet
|
||||||
|
(err.status_code === 500 && err.body?.error === "backup_disabled")
|
||||||
) {
|
) {
|
||||||
|
// core is down because of restore, keep trying
|
||||||
|
this._scheduleLoadBackupInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// core seems to be back up restored
|
||||||
|
if (err.status_code === 404) {
|
||||||
|
this._restoreRunning = undefined;
|
||||||
|
this._backupId = undefined;
|
||||||
window.location.replace("/");
|
window.location.replace("/");
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _scheduleCheckRestoreStatus(): void {
|
this._error = err?.message || "Cannot get backup info";
|
||||||
setTimeout(() => this._checkRestoreStatus(), 1000);
|
|
||||||
|
// if we are in an unknown state, show upload
|
||||||
|
if (this._view === "loading") {
|
||||||
|
this._view = "upload";
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showBackupDialog(): void {
|
const {
|
||||||
showHassioBackupDialog(this, {
|
last_non_idle_event: lastNonIdleEvent,
|
||||||
slug: this._backupSlug!,
|
state: currentState,
|
||||||
onboarding: true,
|
backups,
|
||||||
localize: this.localize,
|
} = onboardingInfo;
|
||||||
onRestoring: () => {
|
|
||||||
this._restoring = true;
|
this._backupInfo = {
|
||||||
this._scheduleCheckRestoreStatus();
|
state: currentState,
|
||||||
},
|
last_non_idle_event: lastNonIdleEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._backupId) {
|
||||||
|
this._backup = backups.find(
|
||||||
|
({ backup_id }) => backup_id === this._backupId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const failedRestore =
|
||||||
|
lastNonIdleEvent?.manager_state === "restore_backup" &&
|
||||||
|
lastNonIdleEvent?.state === "failed";
|
||||||
|
|
||||||
|
if (failedRestore) {
|
||||||
|
this._failed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._restoreRunning) {
|
||||||
|
this._view = "status";
|
||||||
|
if (failedRestore || currentState !== "restore_backup") {
|
||||||
|
this._failed = true;
|
||||||
|
this._restoreRunning = undefined;
|
||||||
|
} else {
|
||||||
|
this._scheduleLoadBackupInfo();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._backup &&
|
||||||
|
// after backup was uploaded
|
||||||
|
(lastNonIdleEvent?.manager_state === "receive_backup" ||
|
||||||
|
// when restore was confirmed but failed to start (for example, encryption key was wrong)
|
||||||
|
failedRestore)
|
||||||
|
) {
|
||||||
|
if (!this.supervisor && this._backup.homeassistant_included) {
|
||||||
|
this._selectedData = {
|
||||||
|
homeassistant_included: true,
|
||||||
|
folders: [],
|
||||||
|
addons: [],
|
||||||
|
homeassistant_version: this._backup.homeassistant_version,
|
||||||
|
database_included: this._backup.database_included,
|
||||||
|
};
|
||||||
|
// skip select data when supervisor is not available and backup includes HA
|
||||||
|
this._view = "confirm_restore";
|
||||||
|
} else {
|
||||||
|
this._view = "select_data";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// show upload as default
|
||||||
|
this._view = "upload";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _scheduleLoadBackupInfo() {
|
||||||
|
setTimeout(() => this._loadBackupInfo(), STATUS_INTERVAL_IN_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _backupUploaded(ev: CustomEvent) {
|
||||||
|
this._backupId = ev.detail.backupId;
|
||||||
|
await this._loadBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _restoreStarted() {
|
||||||
|
if (this._backupInfo) {
|
||||||
|
this._backupInfo.state = "restore_backup";
|
||||||
|
}
|
||||||
|
this._view = "status";
|
||||||
|
this._restoreRunning = true;
|
||||||
|
await this._loadBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _back() {
|
||||||
|
if (this._view === "upload" || (this._view === "status" && this._failed)) {
|
||||||
|
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||||
|
} else {
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.cancel_restore.title"
|
||||||
|
),
|
||||||
|
text: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.cancel_restore.text"
|
||||||
|
),
|
||||||
|
confirmText: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.cancel_restore.yes"
|
||||||
|
),
|
||||||
|
dismissText: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.cancel_restore.no"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
private _restore(ev: CustomEvent) {
|
||||||
return [
|
if (!this._backup || !ev.detail.selectedData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._selectedData = ev.detail.selectedData;
|
||||||
|
|
||||||
|
this._view = "confirm_restore";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _reupload() {
|
||||||
|
this._backup = undefined;
|
||||||
|
this._backupId = undefined;
|
||||||
|
this._view = "upload";
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = [
|
||||||
onBoardingStyles,
|
onBoardingStyles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
position: relative;
|
||||||
}
|
}
|
||||||
hassio-upload-backup {
|
ha-icon-button-arrow-prev {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.footer {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
width: 100%;
|
padding: 32px;
|
||||||
|
}
|
||||||
|
.backup-summary-wrapper {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -13,8 +13,6 @@ class OnboardingWelcome extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
@property({ type: Boolean }) public supervisor = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
|
||||||
@@ -24,11 +22,9 @@ class OnboardingWelcome extends LitElement {
|
|||||||
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
${this.localize("ui.panel.page-onboarding.welcome.start")}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
|
|
||||||
${this.supervisor
|
<ha-button @click=${this._restoreBackup}>
|
||||||
? html`<ha-button @click=${this._restoreBackup}>
|
|
||||||
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
|
||||||
</ha-button>`
|
</ha-button>
|
||||||
: nothing}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { css, html, LitElement, type CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../components/ha-card";
|
||||||
|
import "../../components/ha-circular-progress";
|
||||||
|
import "../../components/ha-alert";
|
||||||
|
import "../../components/ha-button";
|
||||||
|
import "../../panels/config/backup/components/ha-backup-details-restore";
|
||||||
|
import "../../panels/config/backup/components/ha-backup-details-summary";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import type { BackupContentExtended } from "../../data/backup";
|
||||||
|
|
||||||
|
@customElement("onboarding-restore-backup-details")
|
||||||
|
class OnboardingRestoreBackupDetails extends LitElement {
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public backup!: BackupContentExtended;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
${this.backup.homeassistant_included
|
||||||
|
? html`<ha-backup-details-restore
|
||||||
|
.backup=${this.backup}
|
||||||
|
.localize=${this.localize}
|
||||||
|
translation-key-panel="page-onboarding.restore"
|
||||||
|
ha-required
|
||||||
|
></ha-backup-details-restore>`
|
||||||
|
: html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.home_assistant_missing"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
}
|
||||||
|
ha-backup-details-restore {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-restore-backup-details": OnboardingRestoreBackupDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../components/ha-card";
|
||||||
|
import "../../components/ha-alert";
|
||||||
|
import "../../components/buttons/ha-progress-button";
|
||||||
|
import "../../components/ha-password-field";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import {
|
||||||
|
CORE_LOCAL_AGENT,
|
||||||
|
HASSIO_LOCAL_AGENT,
|
||||||
|
type BackupContentExtended,
|
||||||
|
type BackupData,
|
||||||
|
} from "../../data/backup";
|
||||||
|
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
|
||||||
|
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("onboarding-restore-backup-restore")
|
||||||
|
class OnboardingRestoreBackupRestore extends LitElement {
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public backup!: BackupContentExtended;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public selectedData!: BackupData;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public supervisor = false;
|
||||||
|
|
||||||
|
@state() private _encryptionKey = "";
|
||||||
|
|
||||||
|
@state() private _encryptionKeyWrong = false;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _loading = false;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const agentId = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
|
||||||
|
const backupProtected = this.backup.agents[agentId].protected;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.backup.homeassistant_included &&
|
||||||
|
!this.supervisor &&
|
||||||
|
(this.backup.addons.length > 0 || this.backup.folders.length > 0)
|
||||||
|
? html`<ha-alert alert-type="warning" class="supervisor-warning">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.addons_unsupported"
|
||||||
|
)}
|
||||||
|
</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize("ui.panel.page-onboarding.restore.restore")}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
|
||||||
|
: nothing}
|
||||||
|
<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.confirm_restore_full_backup_text"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${backupProtected
|
||||||
|
? html`<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.title"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${this._encryptionKeyWrong
|
||||||
|
? html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-password-field
|
||||||
|
.disabled=${this._loading}
|
||||||
|
@input=${this._encryptionKeyChanged}
|
||||||
|
.label=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.encryption.input_label"
|
||||||
|
)}
|
||||||
|
.value=${this._encryptionKey}
|
||||||
|
></ha-password-field>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<ha-progress-button
|
||||||
|
.progress=${this._loading}
|
||||||
|
.disabled=${this._loading ||
|
||||||
|
(backupProtected && this._encryptionKey === "")}
|
||||||
|
@click=${this._startRestore}
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.details.restore.action"
|
||||||
|
)}
|
||||||
|
</ha-progress-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _encryptionKeyChanged(ev): void {
|
||||||
|
this._encryptionKey = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _startRestore(ev: CustomEvent): Promise<void> {
|
||||||
|
const button = ev.currentTarget as HaProgressButton;
|
||||||
|
this._loading = true;
|
||||||
|
this._error = undefined;
|
||||||
|
this._encryptionKeyWrong = false;
|
||||||
|
|
||||||
|
const backupAgent = this.supervisor ? HASSIO_LOCAL_AGENT : CORE_LOCAL_AGENT;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await restoreOnboardingBackup({
|
||||||
|
agent_id: backupAgent,
|
||||||
|
backup_id: this.backup.backup_id,
|
||||||
|
password: this._encryptionKey || undefined,
|
||||||
|
restore_addons: this.selectedData.addons.map((addon) => addon.slug),
|
||||||
|
restore_database: this.selectedData.database_included,
|
||||||
|
restore_folders: this.selectedData.folders,
|
||||||
|
});
|
||||||
|
button.actionSuccess();
|
||||||
|
fireEvent(this, "restore-started");
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.error === "Request error") {
|
||||||
|
// core can shutdown before we get a response
|
||||||
|
button.actionSuccess();
|
||||||
|
fireEvent(this, "restore-started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.actionError();
|
||||||
|
if (err.body?.code === "incorrect_password") {
|
||||||
|
this._encryptionKeyWrong = true;
|
||||||
|
} else {
|
||||||
|
this._error =
|
||||||
|
err.body?.message || err.message || "Unknown error occurred";
|
||||||
|
}
|
||||||
|
this._loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.supervisor-warning {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-restore-backup-restore": OnboardingRestoreBackupRestore;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"restore-started";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../components/ha-card";
|
||||||
|
import "../../components/ha-circular-progress";
|
||||||
|
import "../../components/ha-alert";
|
||||||
|
import "../../components/ha-button";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import type { BackupOnboardingInfo } from "../../data/backup_onboarding";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { navigate } from "../../common/navigate";
|
||||||
|
import { removeSearchParam } from "../../common/url/search-params";
|
||||||
|
|
||||||
|
@customElement("onboarding-restore-backup-status")
|
||||||
|
class OnboardingRestoreBackupStatus extends LitElement {
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public backupInfo!: BackupOnboardingInfo;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.${this.backupInfo.state === "restore_backup" ? "in_progress" : "failed"}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this.backupInfo.state === "restore_backup"
|
||||||
|
? html`
|
||||||
|
<div class="loading">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.in_progress_description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-alert alert-type="error">
|
||||||
|
${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.failed_status_description"
|
||||||
|
)}
|
||||||
|
</ha-alert>
|
||||||
|
${this.backupInfo.last_non_idle_event?.reason
|
||||||
|
? html`
|
||||||
|
<div class="failed">
|
||||||
|
<h4>Error:</h4>
|
||||||
|
${this.backupInfo.last_non_idle_event?.reason}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
${this.backupInfo.state !== "restore_backup"
|
||||||
|
? html`<div class="card-actions">
|
||||||
|
<ha-button @click=${this._uploadAnother} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._home} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.home`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _uploadAnother() {
|
||||||
|
fireEvent(this, "show-backup-upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _home() {
|
||||||
|
navigate(`${location.pathname}?${removeSearchParam("page")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.failed {
|
||||||
|
padding: 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-restore-backup-status": OnboardingRestoreBackupStatus;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"restore-started";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { mdiFolderUpload } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../components/ha-card";
|
||||||
|
import "../../components/ha-file-upload";
|
||||||
|
import "../../components/ha-alert";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||||
|
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||||
|
import {
|
||||||
|
CORE_LOCAL_AGENT,
|
||||||
|
HASSIO_LOCAL_AGENT,
|
||||||
|
SUPPORTED_UPLOAD_FORMAT,
|
||||||
|
} from "../../data/backup";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import { uploadOnboardingBackup } from "../../data/backup_onboarding";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"backup-uploaded": { backupId: string };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@customElement("onboarding-restore-backup-upload")
|
||||||
|
class OnboardingRestoreBackupUpload extends LitElement {
|
||||||
|
@property({ type: Boolean }) public supervisor = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@state() private _uploading = false;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ha-card
|
||||||
|
.header=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_backup"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<ha-file-upload
|
||||||
|
.uploading=${this._uploading}
|
||||||
|
.icon=${mdiFolderUpload}
|
||||||
|
accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||||
|
.localize=${this.localize}
|
||||||
|
.label=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_input_label"
|
||||||
|
)}
|
||||||
|
.secondary=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_secondary"
|
||||||
|
)}
|
||||||
|
.supports=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.upload_supports_tar"
|
||||||
|
)}
|
||||||
|
.deleteLabel=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.delete"
|
||||||
|
)}
|
||||||
|
.uploadingLabel=${this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.uploading"
|
||||||
|
)}
|
||||||
|
@file-picked=${this._filePicked}
|
||||||
|
></ha-file-upload>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||||
|
this._error = undefined;
|
||||||
|
const file = ev.detail.files[0];
|
||||||
|
|
||||||
|
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.unsupported.title"
|
||||||
|
),
|
||||||
|
text: this.localize(
|
||||||
|
"ui.panel.page-onboarding.restore.unsupported.text"
|
||||||
|
),
|
||||||
|
confirmText: this.localize("ui.panel.page-onboarding.restore.ok"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentIds = this.supervisor
|
||||||
|
? [HASSIO_LOCAL_AGENT]
|
||||||
|
: [CORE_LOCAL_AGENT];
|
||||||
|
|
||||||
|
this._uploading = true;
|
||||||
|
try {
|
||||||
|
const { backup_id } = await uploadOnboardingBackup(file, agentIds);
|
||||||
|
fireEvent(this, "backup-uploaded", { backupId: backup_id });
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error =
|
||||||
|
typeof err.body === "string"
|
||||||
|
? err.body
|
||||||
|
: err.body?.message || err.message || "Unknown error occurred";
|
||||||
|
} finally {
|
||||||
|
this._uploading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"onboarding-restore-backup-upload": OnboardingRestoreBackupUpload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
title: "",
|
title: "",
|
||||||
|
label: localize("ui.panel.config.generic.headers.actions"),
|
||||||
type: "overflow-menu",
|
type: "overflow-menu",
|
||||||
showNarrow: true,
|
showNarrow: true,
|
||||||
hideable: false,
|
hideable: false,
|
||||||
|
|||||||
@@ -329,6 +329,9 @@ class DialogAreaDetail extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
ha-aliases-editor,
|
ha-aliases-editor,
|
||||||
ha-entity-picker,
|
ha-entity-picker,
|
||||||
ha-floor-picker,
|
ha-floor-picker,
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { stringCompare } from "../../../common/string/compare";
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import { stripDiacritics } from "../../../common/string/strip-diacritics";
|
|
||||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import { deepEqual } from "../../../common/util/deep-equal";
|
import { deepEqual } from "../../../common/util/deep-equal";
|
||||||
import "../../../components/ha-dialog";
|
import "../../../components/ha-dialog";
|
||||||
import type { HaDialog } from "../../../components/ha-dialog";
|
import type { HaDialog } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-dialog-header";
|
import "../../../components/ha-dialog-header";
|
||||||
import "../../../components/ha-md-divider";
|
|
||||||
import "../../../components/ha-domain-icon";
|
import "../../../components/ha-domain-icon";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-button-prev";
|
import "../../../components/ha-icon-button-prev";
|
||||||
import "../../../components/ha-icon-next";
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/ha-md-divider";
|
||||||
import "../../../components/ha-md-list";
|
import "../../../components/ha-md-list";
|
||||||
import "../../../components/ha-md-list-item";
|
import "../../../components/ha-md-list-item";
|
||||||
import "../../../components/ha-service-icon";
|
import "../../../components/ha-service-icon";
|
||||||
@@ -45,7 +44,6 @@ import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
|
|||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { getStripDiacriticsFn } from "../../../util/fuse";
|
|
||||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||||
|
|
||||||
@@ -202,10 +200,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
ignoreLocation: true,
|
ignoreLocation: true,
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
minMatchCharLength: Math.min(filter.length, 2),
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
getFn: getStripDiacriticsFn,
|
ignoreDiacritics: true,
|
||||||
};
|
};
|
||||||
const fuse = new Fuse(items, options);
|
const fuse = new Fuse(items, options);
|
||||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
return fuse.search(filter).map((result) => result.item);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mdiCog, mdiHarddisk, mdiNas } from "@mdi/js";
|
import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -41,13 +41,6 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
|
|
||||||
@state() private value?: string[];
|
@state() private value?: string[];
|
||||||
|
|
||||||
private _availableAgents = memoizeOne(
|
|
||||||
(agents: BackupAgent[], cloudStatus: CloudStatus) =>
|
|
||||||
agents.filter(
|
|
||||||
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value ?? DEFAULT_AGENTS;
|
return this.value ?? DEFAULT_AGENTS;
|
||||||
}
|
}
|
||||||
@@ -86,41 +79,46 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
private _availableAgents = memoizeOne(
|
||||||
const agents = this._availableAgents(this.agents, this.cloudStatus);
|
(agents: BackupAgent[], cloudStatus: CloudStatus) =>
|
||||||
return html`
|
agents.filter(
|
||||||
${agents.length > 0
|
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in
|
||||||
? html`
|
)
|
||||||
<ha-md-list>
|
|
||||||
${agents.map((agent) => {
|
|
||||||
const agentId = agent.agent_id;
|
|
||||||
const domain = computeDomain(agentId);
|
|
||||||
const name = computeBackupAgentName(
|
|
||||||
this.hass.localize,
|
|
||||||
agentId,
|
|
||||||
this.agents
|
|
||||||
);
|
);
|
||||||
const description = this._description(agentId);
|
|
||||||
const noCloudSubscription =
|
private _unavailableAgents = memoizeOne(
|
||||||
agentId === CLOUD_AGENT &&
|
(
|
||||||
this.cloudStatus.logged_in &&
|
agents: BackupAgent[],
|
||||||
!this.cloudStatus.active_subscription;
|
cloudStatus: CloudStatus,
|
||||||
|
selectedAgentIds: string[]
|
||||||
|
) => {
|
||||||
|
const availableAgentIds = this._availableAgents(agents, cloudStatus).map(
|
||||||
|
(agent) => agent.agent_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return selectedAgentIds
|
||||||
|
.filter((agent) => !availableAgentIds.includes(agent))
|
||||||
|
.map<BackupAgent>((id) => ({
|
||||||
|
agent_id: id,
|
||||||
|
name: id.split(".")[1] || id, // Use the id as name as it is not available in the list
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _renderAgentIcon(agentId: string) {
|
||||||
|
if (isLocalAgent(agentId)) {
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon .path=${mdiHarddisk} slot="start"></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNetworkMountAgent(agentId)) {
|
||||||
|
return html`<ha-svg-icon .path=${mdiNas} slot="start"></ha-svg-icon>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = computeDomain(agentId);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item>
|
|
||||||
${isLocalAgent(agentId)
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon .path=${mdiHarddisk} slot="start">
|
|
||||||
</ha-svg-icon>
|
|
||||||
`
|
|
||||||
: isNetworkMountAgent(agentId)
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiNas}
|
|
||||||
slot="start"
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<img
|
<img
|
||||||
.src=${brandsUrl({
|
.src=${brandsUrl({
|
||||||
domain,
|
domain,
|
||||||
@@ -133,7 +131,42 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
alt=""
|
alt=""
|
||||||
slot="start"
|
slot="start"
|
||||||
/>
|
/>
|
||||||
`}
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const availableAgents = this._availableAgents(
|
||||||
|
this.agents,
|
||||||
|
this.cloudStatus
|
||||||
|
);
|
||||||
|
const unavailableAgents = this._unavailableAgents(
|
||||||
|
this.agents,
|
||||||
|
this.cloudStatus,
|
||||||
|
this._value
|
||||||
|
);
|
||||||
|
|
||||||
|
const allAgents = [...availableAgents, ...unavailableAgents];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${allAgents.length > 0
|
||||||
|
? html`
|
||||||
|
<ha-md-list>
|
||||||
|
${availableAgents.map((agent) => {
|
||||||
|
const agentId = agent.agent_id;
|
||||||
|
const name = computeBackupAgentName(
|
||||||
|
this.hass.localize,
|
||||||
|
agentId,
|
||||||
|
allAgents
|
||||||
|
);
|
||||||
|
const description = this._description(agentId);
|
||||||
|
const noCloudSubscription =
|
||||||
|
agentId === CLOUD_AGENT &&
|
||||||
|
this.cloudStatus.logged_in &&
|
||||||
|
!this.cloudStatus.active_subscription;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item>
|
||||||
|
${this._renderAgentIcon(agentId)}
|
||||||
<div slot="headline" class="name">${name}</div>
|
<div slot="headline" class="name">${name}</div>
|
||||||
${description
|
${description
|
||||||
? html`<div slot="supporting-text">${description}</div>`
|
? html`<div slot="supporting-text">${description}</div>`
|
||||||
@@ -151,14 +184,44 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
<ha-switch
|
<ha-switch
|
||||||
slot="end"
|
slot="end"
|
||||||
id=${agentId}
|
id=${agentId}
|
||||||
.checked=${!noCloudSubscription &&
|
.checked=${this._value.includes(agentId)}
|
||||||
this._value.includes(agentId)}
|
.disabled=${noCloudSubscription &&
|
||||||
.disabled=${noCloudSubscription}
|
!this._value.includes(agentId)}
|
||||||
@change=${this._agentToggled}
|
@change=${this._agentToggled}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
${unavailableAgents.length > 0 && this.showSettings
|
||||||
|
? html`
|
||||||
|
<p class="heading">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.agents.unavailable_agents"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${unavailableAgents.map((agent) => {
|
||||||
|
const agentId = agent.agent_id;
|
||||||
|
const name = computeBackupAgentName(
|
||||||
|
this.hass.localize,
|
||||||
|
agentId,
|
||||||
|
allAgents
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-list-item>
|
||||||
|
${this._renderAgentIcon(agentId)}
|
||||||
|
<div slot="headline" class="name">${name}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
id=${agentId}
|
||||||
|
slot="end"
|
||||||
|
path=${mdiDelete}
|
||||||
|
@click=${this._deleteAgent}
|
||||||
|
></ha-icon-button>
|
||||||
|
</ha-md-list-item>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@@ -174,6 +237,13 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
navigate(`/config/backup/location/${agentId}`);
|
navigate(`/config/backup/location/${agentId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _deleteAgent(ev): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const agentId = ev.currentTarget.id;
|
||||||
|
this.value = this._value.filter((agent) => agent !== agentId);
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
|
||||||
private _agentToggled(ev) {
|
private _agentToggled(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.currentTarget.checked;
|
const value = ev.currentTarget.checked;
|
||||||
@@ -185,19 +255,8 @@ class HaBackupConfigAgents extends LitElement {
|
|||||||
this.value = this._value.filter((agent) => agent !== agentId);
|
this.value = this._value.filter((agent) => agent !== agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableAgents = this._availableAgents(
|
|
||||||
this.agents,
|
|
||||||
this.cloudStatus
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure we don't have duplicates, agents exist in the list and cloud is logged in
|
// Ensure we don't have duplicates, agents exist in the list and cloud is logged in
|
||||||
this.value = [...new Set(this.value)]
|
this.value = [...new Set(this.value)];
|
||||||
.filter((id) => availableAgents.some((agent) => agent.agent_id === id))
|
|
||||||
.filter(
|
|
||||||
(id) =>
|
|
||||||
id !== CLOUD_AGENT ||
|
|
||||||
(this.cloudStatus.logged_in && this.cloudStatus.active_subscription)
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,8 +378,9 @@ class HaBackupConfigData extends LitElement {
|
|||||||
}
|
}
|
||||||
@media all and (max-width: 450px) {
|
@media all and (max-width: 450px) {
|
||||||
ha-md-select {
|
ha-md-select {
|
||||||
min-width: 160px;
|
min-width: 140px;
|
||||||
width: 160px;
|
width: 140px;
|
||||||
|
--md-filled-field-content-space: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -403,11 +403,11 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
backup_create: html`<a
|
backup_create: html`<a
|
||||||
href=${documentationUrl(
|
href=${documentationUrl(
|
||||||
this.hass,
|
this.hass,
|
||||||
"/integrations/backup#example-backing-up-every-night-at-300-am"
|
"/integrations/backup/#action-backupcreate_automatic"
|
||||||
)}
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>backup.create</a
|
>backup.create_automatic</a
|
||||||
>`,
|
>`,
|
||||||
})}</ha-tip
|
})}</ha-tip
|
||||||
>
|
>
|
||||||
@@ -537,14 +537,22 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
ha-md-list-item {
|
ha-md-list-item {
|
||||||
--md-item-overflow: visible;
|
--md-item-overflow: visible;
|
||||||
}
|
}
|
||||||
ha-md-select,
|
ha-md-select {
|
||||||
ha-time-input {
|
|
||||||
min-width: 210px;
|
min-width: 210px;
|
||||||
}
|
}
|
||||||
@media all and (max-width: 450px) {
|
|
||||||
ha-md-select,
|
|
||||||
ha-time-input {
|
ha-time-input {
|
||||||
|
min-width: 194px;
|
||||||
|
--time-input-flex: 1;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px) {
|
||||||
|
ha-md-select {
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
|
width: 160px;
|
||||||
|
--md-filled-field-content-space: 0;
|
||||||
|
}
|
||||||
|
ha-time-input {
|
||||||
|
min-width: 145px;
|
||||||
|
width: 145px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ha-md-textfield#value {
|
ha-md-textfield#value {
|
||||||
@@ -553,6 +561,16 @@ class HaBackupConfigSchedule extends LitElement {
|
|||||||
ha-md-select#type {
|
ha-md-select#type {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
@media all and (max-width: 450px) {
|
||||||
|
ha-md-textfield#value {
|
||||||
|
min-width: 60px;
|
||||||
|
margin: 0 -8px;
|
||||||
|
}
|
||||||
|
ha-md-select#type {
|
||||||
|
min-width: 120px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
ha-expansion-panel {
|
ha-expansion-panel {
|
||||||
--expansion-panel-summary-padding: 0 16px;
|
--expansion-panel-summary-padding: 0 16px;
|
||||||
--expansion-panel-content-padding: 0 16px;
|
--expansion-panel-content-padding: 0 16px;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface BackupAddonItem {
|
|||||||
|
|
||||||
@customElement("ha-backup-addons-picker")
|
@customElement("ha-backup-addons-picker")
|
||||||
export class HaBackupAddonsPicker extends LitElement {
|
export class HaBackupAddonsPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public addons!: BackupAddonItem[];
|
@property({ attribute: false }) public addons!: BackupAddonItem[];
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export class HaBackupAddonsPicker extends LitElement {
|
|||||||
|
|
||||||
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
||||||
addons.sort((a, b) =>
|
addons.sort((a, b) =>
|
||||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
stringCompare(a.name, b.name, this.hass?.locale?.language)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -47,23 +47,32 @@ interface SelectedItems {
|
|||||||
|
|
||||||
@customElement("ha-backup-data-picker")
|
@customElement("ha-backup-data-picker")
|
||||||
export class HaBackupDataPicker extends LitElement {
|
export class HaBackupDataPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public data!: BackupData;
|
@property({ attribute: false }) public data!: BackupData;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: BackupData;
|
@property({ attribute: false }) public value?: BackupData;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ type: Array, attribute: "required-items" })
|
||||||
|
public requiredItems: string[] = [];
|
||||||
|
|
||||||
|
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||||
|
| "page-onboarding.restore"
|
||||||
|
| "config.backup" = "config.backup";
|
||||||
|
|
||||||
@state() public _addonIcons: Record<string, boolean> = {};
|
@state() public _addonIcons: Record<string, boolean> = {};
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
if (isComponentLoaded(this.hass, "hassio")) {
|
if (this.hass && isComponentLoaded(this.hass, "hassio")) {
|
||||||
this._fetchAddonInfo();
|
this._fetchAddonInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchAddonInfo() {
|
private async _fetchAddonInfo() {
|
||||||
const { addons } = await fetchHassioAddonsInfo(this.hass);
|
const { addons } = await fetchHassioAddonsInfo(this.hass!);
|
||||||
this._addonIcons = addons.reduce<Record<string, boolean>>(
|
this._addonIcons = addons.reduce<Record<string, boolean>>(
|
||||||
(acc, addon) => ({
|
(acc, addon) => ({
|
||||||
...acc,
|
...acc,
|
||||||
@@ -74,16 +83,14 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _homeAssistantItems = memoizeOne(
|
private _homeAssistantItems = memoizeOne(
|
||||||
(data: BackupData, _localize: LocalizeFunc) => {
|
(data: BackupData, localize: LocalizeFunc) => {
|
||||||
const items: CheckBoxItem[] = [];
|
const items: CheckBoxItem[] = [];
|
||||||
|
|
||||||
if (data.homeassistant_included) {
|
if (data.homeassistant_included) {
|
||||||
items.push({
|
items.push({
|
||||||
label: data.database_included
|
label: localize(
|
||||||
? this.hass.localize(
|
`ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
|
||||||
"ui.panel.config.backup.data_picker.settings_and_history"
|
),
|
||||||
)
|
|
||||||
: this.hass.localize("ui.panel.config.backup.data_picker.settings"),
|
|
||||||
id: "config",
|
id: "config",
|
||||||
version: data.homeassistant_version,
|
version: data.homeassistant_version,
|
||||||
});
|
});
|
||||||
@@ -99,18 +106,22 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _localizeFolder(folder: string): string {
|
private _localizeFolder(folder: string): string {
|
||||||
|
const localize = this.localize || this.hass!.localize;
|
||||||
|
|
||||||
switch (folder) {
|
switch (folder) {
|
||||||
case "media":
|
case "media":
|
||||||
return this.hass.localize("ui.panel.config.backup.data_picker.media");
|
return localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.data_picker.media`
|
||||||
|
);
|
||||||
case "share":
|
case "share":
|
||||||
return this.hass.localize(
|
return localize(
|
||||||
"ui.panel.config.backup.data_picker.share_folder"
|
`ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
|
||||||
);
|
);
|
||||||
case "ssl":
|
case "ssl":
|
||||||
return this.hass.localize("ui.panel.config.backup.data_picker.ssl");
|
return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`);
|
||||||
case "addons/local":
|
case "addons/local":
|
||||||
return this.hass.localize(
|
return localize(
|
||||||
"ui.panel.config.backup.data_picker.local_addons"
|
`ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return capitalizeFirstLetter(folder);
|
return capitalizeFirstLetter(folder);
|
||||||
@@ -215,14 +226,13 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const homeAssistantItems = this._homeAssistantItems(
|
const localize = this.localize || this.hass!.localize;
|
||||||
this.data,
|
|
||||||
this.hass.localize
|
const homeAssistantItems = this._homeAssistantItems(this.data, localize);
|
||||||
);
|
|
||||||
|
|
||||||
const addonsItems = this._addonsItems(
|
const addonsItems = this._addonsItems(
|
||||||
this.data,
|
this.data,
|
||||||
this.hass.localize,
|
localize,
|
||||||
this._addonIcons
|
this._addonIcons
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -247,6 +257,7 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
selectedItems.homeassistant.length <
|
selectedItems.homeassistant.length <
|
||||||
homeAssistantItems.length}
|
homeAssistantItems.length}
|
||||||
@change=${this._sectionChanged}
|
@change=${this._sectionChanged}
|
||||||
|
?disabled=${this.requiredItems.length > 0}
|
||||||
></ha-checkbox>
|
></ha-checkbox>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
<div class="items">
|
<div class="items">
|
||||||
@@ -266,6 +277,7 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
item.id
|
item.id
|
||||||
)}
|
)}
|
||||||
@change=${this._homeassistantChanged}
|
@change=${this._homeassistantChanged}
|
||||||
|
.disabled=${this.requiredItems.includes(item.id)}
|
||||||
></ha-checkbox>
|
></ha-checkbox>
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
`
|
`
|
||||||
@@ -280,8 +292,8 @@ export class HaBackupDataPicker extends LitElement {
|
|||||||
<ha-formfield>
|
<ha-formfield>
|
||||||
<ha-backup-formfield-label
|
<ha-backup-formfield-label
|
||||||
slot="label"
|
slot="label"
|
||||||
.label=${this.hass.localize(
|
.label=${localize(
|
||||||
"ui.panel.config.backup.data_picker.addons"
|
`ui.panel.${this.translationKeyPanel}.data_picker.addons`
|
||||||
)}
|
)}
|
||||||
.iconPath=${mdiPuzzle}
|
.iconPath=${mdiPuzzle}
|
||||||
>
|
>
|
||||||
|
|||||||
148
src/panels/config/backup/components/ha-backup-details-restore.ts
Normal file
148
src/panels/config/backup/components/ha-backup-details-restore.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "./ha-backup-data-picker";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import type {
|
||||||
|
BackupContentExtended,
|
||||||
|
BackupData,
|
||||||
|
} from "../../../../data/backup";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("ha-backup-details-restore")
|
||||||
|
class HaBackupDetailsRestore extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ type: Object }) public backup!: BackupContentExtended;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "ha-required" })
|
||||||
|
public haRequired = false;
|
||||||
|
|
||||||
|
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||||
|
| "page-onboarding.restore"
|
||||||
|
| "config.backup" = "config.backup";
|
||||||
|
|
||||||
|
@state() private _selectedData?: BackupData;
|
||||||
|
|
||||||
|
protected willUpdate() {
|
||||||
|
if (!this.hasUpdated && this.haRequired) {
|
||||||
|
this._selectedData = {
|
||||||
|
homeassistant_included: true,
|
||||||
|
folders: [],
|
||||||
|
addons: [],
|
||||||
|
homeassistant_version: this.backup.homeassistant_version,
|
||||||
|
database_included: this.backup.database_included,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.restore.title`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-backup-data-picker
|
||||||
|
.translationKeyPanel=${this.translationKeyPanel}
|
||||||
|
.localize=${this.localize}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this.backup}
|
||||||
|
.value=${this._selectedData}
|
||||||
|
@value-changed=${this._selectedBackupChanged}
|
||||||
|
.requiredItems=${this._isHomeAssistantRequired(this.haRequired)}
|
||||||
|
>
|
||||||
|
</ha-backup-data-picker>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<ha-button
|
||||||
|
@click=${this._restore}
|
||||||
|
.disabled=${this._isRestoreDisabled}
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.restore.action`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _restore() {
|
||||||
|
fireEvent(this, "backup-restore", { selectedData: this._selectedData });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectedBackupChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._selectedData = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isHomeAssistantRequired = memoizeOne((required: boolean) =>
|
||||||
|
required ? ["config"] : []
|
||||||
|
);
|
||||||
|
|
||||||
|
private get _isRestoreDisabled(): boolean {
|
||||||
|
return (
|
||||||
|
!this._selectedData ||
|
||||||
|
(this.haRequired && !this._selectedData.homeassistant_included) ||
|
||||||
|
!(
|
||||||
|
this._selectedData?.database_included ||
|
||||||
|
this._selectedData?.homeassistant_included ||
|
||||||
|
this._selectedData.addons.length ||
|
||||||
|
this._selectedData.folders.length
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
max-width: 690px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 24px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-md-list-item {
|
||||||
|
--md-list-item-leading-space: 0;
|
||||||
|
--md-list-item-trailing-space: 0;
|
||||||
|
--md-list-item-two-line-container-height: 64px;
|
||||||
|
}
|
||||||
|
ha-md-list-item [slot="supporting-text"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-backup-details-restore": HaBackupDetailsRestore;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"backup-restore": { selectedData?: BackupData };
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/panels/config/backup/components/ha-backup-details-summary.ts
Normal file
153
src/panels/config/backup/components/ha-backup-details-summary.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatDateTimeWithBrowserDefaults,
|
||||||
|
} from "../../../../common/datetime/format_date_time";
|
||||||
|
import {
|
||||||
|
computeBackupSize,
|
||||||
|
computeBackupType,
|
||||||
|
type BackupContentExtended,
|
||||||
|
} from "../../../../data/backup";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { bytesToString } from "../../../../util/bytes-to-string";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"show-backup-upload": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-backup-details-summary")
|
||||||
|
class HaBackupDetailsSummary extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||||
|
|
||||||
|
@property({ type: Object }) public backup!: BackupContentExtended;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "hassio" }) public isHassio = false;
|
||||||
|
|
||||||
|
@property({ attribute: "translation-key-panel" }) public translationKeyPanel:
|
||||||
|
| "page-onboarding.restore"
|
||||||
|
| "config.backup" = "config.backup";
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "show-upload-another" })
|
||||||
|
public showUploadAnother = false;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const backupDate = new Date(this.backup.date);
|
||||||
|
const formattedDate = this.hass
|
||||||
|
? formatDateTime(backupDate, this.hass.locale, this.hass.config)
|
||||||
|
: formatDateTimeWithBrowserDefaults(backupDate);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-header">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.title`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<ha-md-list class="summary">
|
||||||
|
${this.translationKeyPanel === "config.backup"
|
||||||
|
? html`<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize("ui.panel.config.backup.backup_type")}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.config.backup.type.${computeBackupType(this.backup, this.isHassio)}`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</ha-md-list-item>`
|
||||||
|
: nothing}
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.size`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text">
|
||||||
|
${bytesToString(computeBackupSize(this.backup))}
|
||||||
|
</span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
<ha-md-list-item>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.${this.translationKeyPanel}.details.summary.created`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="supporting-text"> ${formattedDate} </span>
|
||||||
|
</ha-md-list-item>
|
||||||
|
</ha-md-list>
|
||||||
|
</div>
|
||||||
|
${this.showUploadAnother
|
||||||
|
? html`<div class="card-actions">
|
||||||
|
<ha-button @click=${this._uploadAnother} destructive>
|
||||||
|
${this.localize(
|
||||||
|
`ui.panel.page-onboarding.restore.details.summary.upload_another`
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _uploadAnother() {
|
||||||
|
fireEvent(this, "show-backup-upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
max-width: 690px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 24px;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
ha-md-list {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-md-list-item {
|
||||||
|
--md-list-item-leading-space: 0;
|
||||||
|
--md-list-item-trailing-space: 0;
|
||||||
|
--md-list-item-two-line-container-height: 64px;
|
||||||
|
}
|
||||||
|
ha-md-list.summary ha-md-list-item {
|
||||||
|
--md-list-item-supporting-text-size: 1rem;
|
||||||
|
--md-list-item-label-text-size: 0.875rem;
|
||||||
|
|
||||||
|
--md-list-item-label-text-color: var(--secondary-text-color);
|
||||||
|
--md-list-item-supporting-text-color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
ha-md-list-item [slot="supporting-text"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-backup-details-summary": HaBackupDetailsSummary;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { mdiCalendarSync, mdiGestureTap } from "@mdi/js";
|
import { mdiCalendarSync, mdiGestureTap, mdiPuzzle } from "@mdi/js";
|
||||||
import type { CSSResultGroup } from "lit";
|
import type { CSSResultGroup } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||||
import "../../../../../components/ha-button";
|
import "../../../../../components/ha-button";
|
||||||
import "../../../../../components/ha-card";
|
import "../../../../../components/ha-card";
|
||||||
import "../../../../../components/ha-icon-next";
|
import "../../../../../components/ha-icon-next";
|
||||||
import "../../../../../components/ha-md-list";
|
import "../../../../../components/ha-md-list";
|
||||||
import "../../../../../components/ha-md-list-item";
|
import "../../../../../components/ha-md-list-item";
|
||||||
|
import type { BackupContent, BackupType } from "../../../../../data/backup";
|
||||||
import {
|
import {
|
||||||
computeBackupSize,
|
computeBackupSize,
|
||||||
type BackupContent,
|
computeBackupType,
|
||||||
|
getBackupTypes,
|
||||||
} from "../../../../../data/backup";
|
} from "../../../../../data/backup";
|
||||||
import { haStyle } from "../../../../../resources/styles";
|
import { haStyle } from "../../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../../types";
|
import type { HomeAssistant } from "../../../../../types";
|
||||||
@@ -21,6 +24,12 @@ interface BackupStats {
|
|||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TYPE_ICONS: Record<BackupType, string> = {
|
||||||
|
automatic: mdiCalendarSync,
|
||||||
|
manual: mdiGestureTap,
|
||||||
|
addon_update: mdiPuzzle,
|
||||||
|
};
|
||||||
|
|
||||||
const computeBackupStats = (backups: BackupContent[]): BackupStats =>
|
const computeBackupStats = (backups: BackupContent[]): BackupStats =>
|
||||||
backups.reduce(
|
backups.reduce(
|
||||||
(stats, backup) => {
|
(stats, backup) => {
|
||||||
@@ -37,23 +46,22 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public backups: BackupContent[] = [];
|
@property({ attribute: false }) public backups: BackupContent[] = [];
|
||||||
|
|
||||||
private _automaticStats = memoizeOne((backups: BackupContent[]) => {
|
private _stats = memoizeOne(
|
||||||
const automaticBackups = backups.filter(
|
(
|
||||||
(backup) => backup.with_automatic_settings
|
backups: BackupContent[],
|
||||||
|
isHassio: boolean
|
||||||
|
): [BackupType, BackupStats][] =>
|
||||||
|
getBackupTypes(isHassio).map((type) => {
|
||||||
|
const backupsOfType = backups.filter(
|
||||||
|
(backup) => computeBackupType(backup, isHassio) === type
|
||||||
);
|
);
|
||||||
return computeBackupStats(automaticBackups);
|
return [type, computeBackupStats(backupsOfType)] as const;
|
||||||
});
|
})
|
||||||
|
|
||||||
private _manualStats = memoizeOne((backups: BackupContent[]) => {
|
|
||||||
const manualBackups = backups.filter(
|
|
||||||
(backup) => !backup.with_automatic_settings
|
|
||||||
);
|
);
|
||||||
return computeBackupStats(manualBackups);
|
|
||||||
});
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const automaticStats = this._automaticStats(this.backups);
|
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||||
const manualStats = this._manualStats(this.backups);
|
const stats = this._stats(this.backups, isHassio);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class="my-backups">
|
<ha-card class="my-backups">
|
||||||
@@ -62,44 +70,32 @@ class HaBackupOverviewBackups extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<ha-md-list>
|
<ha-md-list>
|
||||||
|
${stats.map(
|
||||||
|
([type, { count, size }]) => html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
type="link"
|
type="link"
|
||||||
href="/config/backup/backups?type=automatic"
|
href="/config/backup/backups?type=${type}"
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon>
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${TYPE_ICONS[type]}
|
||||||
|
></ha-svg-icon>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.overview.backups.automatic",
|
`ui.panel.config.backup.overview.backups.${type}`,
|
||||||
{ count: automaticStats.count }
|
{ count }
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div slot="supporting-text">
|
<div slot="supporting-text">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.overview.backups.total_size",
|
"ui.panel.config.backup.overview.backups.total_size",
|
||||||
{ size: bytesToString(automaticStats.size, 1) }
|
{ size: bytesToString(size) }
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ha-icon-next slot="end"></ha-icon-next>
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
<ha-md-list-item
|
`
|
||||||
type="link"
|
|
||||||
href="/config/backup/backups?type=manual"
|
|
||||||
>
|
|
||||||
<ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon>
|
|
||||||
<div slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.overview.backups.manual",
|
|
||||||
{ count: manualStats.count }
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
<div slot="supporting-text">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.overview.backups.total_size",
|
|
||||||
{ size: bytesToString(manualStats.size, 1) }
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<ha-icon-next slot="end"></ha-icon-next>
|
|
||||||
</ha-md-list-item>
|
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-icon-next";
|
||||||
|
import "../../../../components/ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||||
|
import "../../../../components/ha-md-list";
|
||||||
|
import "../../../../components/ha-md-list-item";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import "../../../../components/ha-password-field";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
|
import {
|
||||||
|
canDecryptBackupOnDownload,
|
||||||
|
getPreferredAgentForDownload,
|
||||||
|
} from "../../../../data/backup";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { downloadBackupFile } from "../helper/download_backup";
|
||||||
|
import type { DownloadDecryptedBackupDialogParams } from "./show-dialog-download-decrypted-backup";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-download-decrypted-backup")
|
||||||
|
class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _params?: DownloadDecryptedBackupDialogParams;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
|
@state() private _encryptionKey = "";
|
||||||
|
|
||||||
|
@state() private _error = "";
|
||||||
|
|
||||||
|
public showDialog(params: DownloadDecryptedBackupDialogParams): void {
|
||||||
|
this._opened = true;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._dialog?.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed() {
|
||||||
|
if (this._opened) {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
this._opened = false;
|
||||||
|
this._params = undefined;
|
||||||
|
this._encryptionKey = "";
|
||||||
|
this._error = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._opened || !this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog open @closed=${this._dialogClosed} disable-cancel-action>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.title"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_backup_encrypted",
|
||||||
|
{
|
||||||
|
download_it_encrypted: html`<button
|
||||||
|
class="link"
|
||||||
|
@click=${this._downloadEncrypted}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download_it_encrypted"
|
||||||
|
)}
|
||||||
|
</button>`,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-password-field
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.encryption_key"
|
||||||
|
)}
|
||||||
|
@input=${this._keyChanged}
|
||||||
|
></ha-password-field>
|
||||||
|
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
|
<ha-button @click=${this._cancel}>
|
||||||
|
${this.hass.localize("ui.dialogs.generic.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
|
||||||
|
<ha-button @click=${this._submit}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.download"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cancel() {
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _submit() {
|
||||||
|
if (this._encryptionKey === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await canDecryptBackupOnDownload(
|
||||||
|
this.hass,
|
||||||
|
this._params!.backup.backup_id,
|
||||||
|
this._agentId,
|
||||||
|
this._encryptionKey
|
||||||
|
);
|
||||||
|
downloadBackupFile(
|
||||||
|
this.hass,
|
||||||
|
this._params!.backup.backup_id,
|
||||||
|
this._agentId,
|
||||||
|
this._encryptionKey
|
||||||
|
);
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err?.code === "password_incorrect") {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.incorrect_encryption_key"
|
||||||
|
);
|
||||||
|
} else if (err?.code === "decrypt_not_supported") {
|
||||||
|
this._error = this.hass.localize(
|
||||||
|
"ui.panel.config.backup.dialogs.download.decryption_not_supported"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _keyChanged(ev) {
|
||||||
|
this._encryptionKey = ev.currentTarget.value;
|
||||||
|
this._error = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _agentId() {
|
||||||
|
if (this._params?.agentId) {
|
||||||
|
return this._params.agentId;
|
||||||
|
}
|
||||||
|
return getPreferredAgentForDownload(
|
||||||
|
Object.keys(this._params!.backup.agents)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _downloadEncrypted() {
|
||||||
|
downloadBackupFile(
|
||||||
|
this.hass,
|
||||||
|
this._params!.backup.backup_id,
|
||||||
|
this._agentId
|
||||||
|
);
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-md-dialog {
|
||||||
|
--dialog-content-padding: 8px 24px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-md-dialog {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
div[slot="content"] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.link {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-download-decrypted-backup": DialogDownloadDecryptedBackup;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,10 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
type HASSDomEvent,
|
||||||
|
} from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-header";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
@@ -14,7 +17,10 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
|||||||
import {
|
import {
|
||||||
CORE_LOCAL_AGENT,
|
CORE_LOCAL_AGENT,
|
||||||
HASSIO_LOCAL_AGENT,
|
HASSIO_LOCAL_AGENT,
|
||||||
|
SUPPORTED_UPLOAD_FORMAT,
|
||||||
uploadBackup,
|
uploadBackup,
|
||||||
|
INITIAL_UPLOAD_FORM_DATA,
|
||||||
|
type BackupUploadFileFormData,
|
||||||
} from "../../../../data/backup";
|
} from "../../../../data/backup";
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
@@ -22,16 +28,6 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||||
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||||
|
|
||||||
const SUPPORTED_FORMAT = "application/x-tar";
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
file?: File;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INITIAL_DATA: FormData = {
|
|
||||||
file: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-dialog-upload-backup")
|
@customElement("ha-dialog-upload-backup")
|
||||||
export class DialogUploadBackup
|
export class DialogUploadBackup
|
||||||
extends LitElement
|
extends LitElement
|
||||||
@@ -45,13 +41,13 @@ export class DialogUploadBackup
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _formData?: FormData;
|
@state() private _formData?: BackupUploadFileFormData;
|
||||||
|
|
||||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._formData = INITIAL_DATA;
|
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed() {
|
private _dialogClosed() {
|
||||||
@@ -78,13 +74,18 @@ export class DialogUploadBackup
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
<ha-md-dialog
|
||||||
|
open
|
||||||
|
@closed=${this._dialogClosed}
|
||||||
|
.disableCancelAction=${this._uploading}
|
||||||
|
>
|
||||||
<ha-dialog-header slot="headline">
|
<ha-dialog-header slot="headline">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
|
.disabled=${this._uploading}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
|
|
||||||
<span slot="title">
|
<span slot="title">
|
||||||
@@ -99,7 +100,8 @@ export class DialogUploadBackup
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.uploading=${this._uploading}
|
.uploading=${this._uploading}
|
||||||
.icon=${mdiFolderUpload}
|
.icon=${mdiFolderUpload}
|
||||||
accept=${SUPPORTED_FORMAT}
|
.accept=${SUPPORTED_UPLOAD_FORMAT}
|
||||||
|
.localize=${this.hass.localize}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.input_label"
|
"ui.panel.config.backup.dialogs.upload.input_label"
|
||||||
)}
|
)}
|
||||||
@@ -107,13 +109,17 @@ export class DialogUploadBackup
|
|||||||
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
"ui.panel.config.backup.dialogs.upload.supports_tar"
|
||||||
)}
|
)}
|
||||||
@file-picked=${this._filePicked}
|
@file-picked=${this._filePicked}
|
||||||
|
@files-cleared=${this._filesCleared}
|
||||||
></ha-file-upload>
|
></ha-file-upload>
|
||||||
</div>
|
</div>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
<ha-button @click=${this.closeDialog}
|
<ha-button @click=${this.closeDialog} .disabled=${this._uploading}
|
||||||
>${this.hass.localize("ui.common.cancel")}</ha-button
|
>${this.hass.localize("ui.common.cancel")}</ha-button
|
||||||
>
|
>
|
||||||
<ha-button @click=${this._upload} .disabled=${!this._formValid()}>
|
<ha-button
|
||||||
|
@click=${this._upload}
|
||||||
|
.disabled=${!this._formValid() || this._uploading}
|
||||||
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.action"
|
"ui.panel.config.backup.dialogs.upload.action"
|
||||||
)}
|
)}
|
||||||
@@ -123,7 +129,7 @@ export class DialogUploadBackup
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
|
||||||
this._error = undefined;
|
this._error = undefined;
|
||||||
const file = ev.detail.files[0];
|
const file = ev.detail.files[0];
|
||||||
|
|
||||||
@@ -133,9 +139,14 @@ export class DialogUploadBackup
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _filesCleared() {
|
||||||
|
this._error = undefined;
|
||||||
|
this._formData = INITIAL_UPLOAD_FORM_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
private async _upload() {
|
private async _upload() {
|
||||||
const { file } = this._formData!;
|
const { file } = this._formData!;
|
||||||
if (!file || file.type !== SUPPORTED_FORMAT) {
|
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
"ui.panel.config.backup.dialogs.upload.unsupported.title"
|
||||||
@@ -154,7 +165,7 @@ export class DialogUploadBackup
|
|||||||
|
|
||||||
this._uploading = true;
|
this._uploading = true;
|
||||||
try {
|
try {
|
||||||
await uploadBackup(this.hass!, file, agentIds);
|
await uploadBackup(this.hass, file, agentIds);
|
||||||
this._params!.submit?.();
|
this._params!.submit?.();
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import type { BackupContent } from "../../../../data/backup";
|
||||||
|
|
||||||
|
export interface DownloadDecryptedBackupDialogParams {
|
||||||
|
backup: BackupContent;
|
||||||
|
agentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadDownloadDecryptedBackupDialog = () =>
|
||||||
|
import("./dialog-download-decrypted-backup");
|
||||||
|
|
||||||
|
export const showDownloadDecryptedBackupDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: DownloadDecryptedBackupDialogParams
|
||||||
|
) => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-download-decrypted-backup",
|
||||||
|
dialogImport: loadDownloadDecryptedBackupDialog,
|
||||||
|
dialogParams: params,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import type { CSSResultGroup, TemplateResult } from "lit";
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
import { storage } from "../../../common/decorators/storage";
|
import { storage } from "../../../common/decorators/storage";
|
||||||
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
|
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
@@ -42,9 +43,11 @@ import {
|
|||||||
compareAgents,
|
compareAgents,
|
||||||
computeBackupAgentName,
|
computeBackupAgentName,
|
||||||
computeBackupSize,
|
computeBackupSize,
|
||||||
|
computeBackupType,
|
||||||
deleteBackup,
|
deleteBackup,
|
||||||
generateBackup,
|
generateBackup,
|
||||||
generateBackupWithAutomaticSettings,
|
generateBackupWithAutomaticSettings,
|
||||||
|
getBackupTypes,
|
||||||
isLocalAgent,
|
isLocalAgent,
|
||||||
isNetworkMountAgent,
|
isNetworkMountAgent,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
@@ -74,10 +77,6 @@ interface BackupRow extends DataTableRowData, BackupContent {
|
|||||||
agent_ids: string[];
|
agent_ids: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackupType = "automatic" | "manual";
|
|
||||||
|
|
||||||
const TYPE_ORDER: BackupType[] = ["automatic", "manual"];
|
|
||||||
|
|
||||||
@customElement("ha-config-backup-backups")
|
@customElement("ha-config-backup-backups")
|
||||||
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -141,7 +140,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(localize: LocalizeFunc): DataTableColumnContainer<BackupRow> => ({
|
(
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
maxDisplayedAgents: number
|
||||||
|
): DataTableColumnContainer<BackupRow> => ({
|
||||||
name: {
|
name: {
|
||||||
title: localize("ui.panel.config.backup.name"),
|
title: localize("ui.panel.config.backup.name"),
|
||||||
main: true,
|
main: true,
|
||||||
@@ -172,10 +174,21 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
locations: {
|
locations: {
|
||||||
title: localize("ui.panel.config.backup.locations"),
|
title: localize("ui.panel.config.backup.locations"),
|
||||||
showNarrow: true,
|
showNarrow: true,
|
||||||
minWidth: "60px",
|
// 24 icon size, 4 gap, 16 left and right padding
|
||||||
template: (backup) => html`
|
minWidth: `${maxDisplayedAgents * 24 + (maxDisplayedAgents - 1) * 4 + 32}px`,
|
||||||
|
template: (backup) => {
|
||||||
|
const agentIds = backup.agent_ids;
|
||||||
|
const displayedAgentIds =
|
||||||
|
agentIds.length > maxDisplayedAgents
|
||||||
|
? [...agentIds].splice(0, maxDisplayedAgents - 1)
|
||||||
|
: agentIds;
|
||||||
|
const agentsMore = Math.max(
|
||||||
|
agentIds.length - displayedAgentIds.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return html`
|
||||||
<div style="display: flex; gap: 4px;">
|
<div style="display: flex; gap: 4px;">
|
||||||
${(backup.agent_ids || []).map((agentId) => {
|
${displayedAgentIds.map((agentId) => {
|
||||||
const name = computeBackupAgentName(
|
const name = computeBackupAgentName(
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
agentId,
|
agentId,
|
||||||
@@ -218,8 +231,18 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
${agentsMore
|
||||||
|
? html`
|
||||||
|
<span
|
||||||
|
style="display: flex; align-items: center; font-size: 14px;"
|
||||||
|
>
|
||||||
|
+${agentsMore}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
title: "",
|
title: "",
|
||||||
@@ -253,9 +276,13 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private _groupOrder = memoizeOne(
|
private _groupOrder = memoizeOne(
|
||||||
(activeGrouping: string | undefined, localize: LocalizeFunc) =>
|
(
|
||||||
|
activeGrouping: string | undefined,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
isHassio: boolean
|
||||||
|
) =>
|
||||||
activeGrouping === "formatted_type"
|
activeGrouping === "formatted_type"
|
||||||
? TYPE_ORDER.map((type) =>
|
? getBackupTypes(isHassio).map((type) =>
|
||||||
localize(`ui.panel.config.backup.type.${type}`)
|
localize(`ui.panel.config.backup.type.${type}`)
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
@@ -279,33 +306,48 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
(
|
(
|
||||||
backups: BackupContent[],
|
backups: BackupContent[],
|
||||||
filters: DataTableFiltersValues,
|
filters: DataTableFiltersValues,
|
||||||
localize: LocalizeFunc
|
localize: LocalizeFunc,
|
||||||
|
isHassio: boolean
|
||||||
): BackupRow[] => {
|
): BackupRow[] => {
|
||||||
const typeFilter = filters["ha-filter-states"] as string[] | undefined;
|
const typeFilter = filters["ha-filter-states"] as string[] | undefined;
|
||||||
let filteredBackups = backups;
|
let filteredBackups = backups;
|
||||||
if (typeFilter?.length) {
|
if (typeFilter?.length) {
|
||||||
filteredBackups = filteredBackups.filter(
|
filteredBackups = filteredBackups.filter((backup) => {
|
||||||
(backup) =>
|
const type = computeBackupType(backup, isHassio);
|
||||||
(backup.with_automatic_settings &&
|
return typeFilter.includes(type);
|
||||||
typeFilter.includes("automatic")) ||
|
});
|
||||||
(!backup.with_automatic_settings && typeFilter.includes("manual"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return filteredBackups.map((backup) => {
|
return filteredBackups.map((backup) => {
|
||||||
const type = backup.with_automatic_settings ? "automatic" : "manual";
|
const type = computeBackupType(backup, isHassio);
|
||||||
|
const agentIds = Object.keys(backup.agents);
|
||||||
return {
|
return {
|
||||||
...backup,
|
...backup,
|
||||||
size: computeBackupSize(backup),
|
size: computeBackupSize(backup),
|
||||||
agent_ids: Object.keys(backup.agents).sort(compareAgents),
|
agent_ids: agentIds.sort(compareAgents),
|
||||||
formatted_type: localize(`ui.panel.config.backup.type.${type}`),
|
formatted_type: localize(`ui.panel.config.backup.type.${type}`),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _maxAgents = memoizeOne((data: BackupRow[]): number =>
|
||||||
|
Math.max(...data.map((row) => row.agent_ids.length))
|
||||||
|
);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const backupInProgress =
|
const backupInProgress =
|
||||||
"state" in this.manager && this.manager.state === "in_progress";
|
"state" in this.manager && this.manager.state === "in_progress";
|
||||||
|
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||||
|
const data = this._data(
|
||||||
|
this.backups,
|
||||||
|
this._filters,
|
||||||
|
this.hass.localize,
|
||||||
|
isHassio
|
||||||
|
);
|
||||||
|
const maxDisplayedAgents = Math.min(
|
||||||
|
this._maxAgents(data),
|
||||||
|
this.narrow ? 3 : 5
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
@@ -336,15 +378,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
.initialCollapsedGroups=${this._activeCollapsed}
|
.initialCollapsedGroups=${this._activeCollapsed}
|
||||||
.groupOrder=${this._groupOrder(
|
.groupOrder=${this._groupOrder(
|
||||||
this._activeGrouping,
|
this._activeGrouping,
|
||||||
this.hass.localize
|
this.hass.localize,
|
||||||
|
isHassio
|
||||||
)}
|
)}
|
||||||
@grouping-changed=${this._handleGroupingChanged}
|
@grouping-changed=${this._handleGroupingChanged}
|
||||||
@collapsed-changed=${this._handleCollapseChanged}
|
@collapsed-changed=${this._handleCollapseChanged}
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
@row-click=${this._showBackupDetails}
|
@row-click=${this._showBackupDetails}
|
||||||
.columns=${this._columns(this.hass.localize)}
|
.columns=${this._columns(this.hass.localize, maxDisplayedAgents)}
|
||||||
.data=${this._data(this.backups, this._filters, this.hass.localize)}
|
.data=${data}
|
||||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||||
.searchLabel=${this.hass.localize(
|
.searchLabel=${this.hass.localize(
|
||||||
"ui.panel.config.backup.picker.search"
|
"ui.panel.config.backup.picker.search"
|
||||||
@@ -400,7 +443,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
|
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
|
||||||
.value=${this._filters["ha-filter-states"]}
|
.value=${this._filters["ha-filter-states"]}
|
||||||
.states=${this._states(this.hass.localize)}
|
.states=${this._states(this.hass.localize, isHassio)}
|
||||||
@data-table-filter-changed=${this._filterChanged}
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
slot="filter-pane"
|
slot="filter-pane"
|
||||||
expanded
|
expanded
|
||||||
@@ -425,8 +468,8 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _states = memoizeOne((localize: LocalizeFunc) =>
|
private _states = memoizeOne((localize: LocalizeFunc, isHassio: boolean) =>
|
||||||
TYPE_ORDER.map((type) => ({
|
getBackupTypes(isHassio).map((type) => ({
|
||||||
value: type,
|
value: type,
|
||||||
label: localize(`ui.panel.config.backup.type.${type}`),
|
label: localize(`ui.panel.config.backup.type.${type}`),
|
||||||
}))
|
}))
|
||||||
@@ -496,12 +539,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||||
downloadBackup(
|
downloadBackup(this.hass, this, backup, this.config);
|
||||||
this.hass,
|
|
||||||
this,
|
|
||||||
backup,
|
|
||||||
this.config?.create_backup.password
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { formatDateTime } from "../../../common/datetime/format_date_time";
|
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
@@ -25,27 +24,25 @@ import type {
|
|||||||
BackupConfig,
|
BackupConfig,
|
||||||
BackupContentAgent,
|
BackupContentAgent,
|
||||||
BackupContentExtended,
|
BackupContentExtended,
|
||||||
BackupData,
|
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
|
import "./components/ha-backup-details-summary";
|
||||||
|
import "./components/ha-backup-details-restore";
|
||||||
import {
|
import {
|
||||||
compareAgents,
|
compareAgents,
|
||||||
computeBackupAgentName,
|
computeBackupAgentName,
|
||||||
computeBackupSize,
|
|
||||||
deleteBackup,
|
deleteBackup,
|
||||||
fetchBackupDetails,
|
fetchBackupDetails,
|
||||||
isLocalAgent,
|
isLocalAgent,
|
||||||
isNetworkMountAgent,
|
isNetworkMountAgent,
|
||||||
} from "../../../data/backup";
|
} from "../../../data/backup";
|
||||||
import type { HassioAddonInfo } from "../../../data/hassio/addon";
|
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { brandsUrl } from "../../../util/brands-url";
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
import { bytesToString } from "../../../util/bytes-to-string";
|
|
||||||
import "./components/ha-backup-data-picker";
|
|
||||||
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
|
import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import { downloadBackup } from "./helper/download_backup";
|
import { downloadBackup } from "./helper/download_backup";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
|
||||||
interface Agent extends BackupContentAgent {
|
interface Agent extends BackupContentAgent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -91,10 +88,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
@state() private _selectedData?: BackupData;
|
|
||||||
|
|
||||||
@state() private _addonsInfo?: HassioAddonInfo[];
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
protected firstUpdated(changedProps) {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
|
|
||||||
@@ -110,6 +103,8 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
back-path="/config/backup/backups"
|
back-path="/config/backup/backups"
|
||||||
@@ -153,69 +148,18 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
: !this._backup
|
: !this._backup
|
||||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||||
: html`
|
: html`
|
||||||
<ha-card>
|
<ha-backup-details-summary
|
||||||
<div class="card-header">
|
.backup=${this._backup}
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.summary.title"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-md-list class="summary">
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.summary.size"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${bytesToString(computeBackupSize(this._backup))}
|
|
||||||
</span>
|
|
||||||
</ha-md-list-item>
|
|
||||||
<ha-md-list-item>
|
|
||||||
<span slot="headline">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.summary.created"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">
|
|
||||||
${formatDateTime(
|
|
||||||
new Date(this._backup.date),
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-md-list-item>
|
|
||||||
</ha-md-list>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
<ha-card>
|
|
||||||
<div class="card-header">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.restore.title"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-backup-data-picker
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this._backup}
|
.localize=${this.hass.localize}
|
||||||
.value=${this._selectedData}
|
.isHassio=${isHassio}
|
||||||
@value-changed=${this._selectedBackupChanged}
|
></ha-backup-details-summary>
|
||||||
.addonsInfo=${this._addonsInfo}
|
<ha-backup-details-restore
|
||||||
>
|
.backup=${this._backup}
|
||||||
</ha-backup-data-picker>
|
@backup-restore=${this._restore}
|
||||||
</div>
|
.hass=${this.hass}
|
||||||
<div class="card-actions">
|
.localize=${this.hass.localize}
|
||||||
<ha-button
|
></ha-backup-details-restore>
|
||||||
@click=${this._restore}
|
|
||||||
.disabled=${this._isRestoreDisabled()}
|
|
||||||
class="danger"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.backup.details.restore.action"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@@ -344,30 +288,13 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selectedBackupChanged(ev: CustomEvent) {
|
private _restore(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
if (!this._backup || !ev.detail.selectedData) {
|
||||||
this._selectedData = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isRestoreDisabled() {
|
|
||||||
return (
|
|
||||||
!this._selectedData ||
|
|
||||||
!(
|
|
||||||
this._selectedData?.database_included ||
|
|
||||||
this._selectedData?.homeassistant_included ||
|
|
||||||
this._selectedData.addons.length ||
|
|
||||||
this._selectedData.folders.length
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _restore() {
|
|
||||||
if (!this._backup || !this._selectedData) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showRestoreBackupDialog(this, {
|
showRestoreBackupDialog(this, {
|
||||||
backup: this._backup,
|
backup: this._backup,
|
||||||
selectedData: this._selectedData,
|
selectedData: ev.detail.selectedData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,13 +328,7 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _downloadBackup(agentId?: string): Promise<void> {
|
private async _downloadBackup(agentId?: string): Promise<void> {
|
||||||
await downloadBackup(
|
await downloadBackup(this.hass, this, this._backup!, this.config, agentId);
|
||||||
this.hass,
|
|
||||||
this,
|
|
||||||
this._backup!,
|
|
||||||
this.config?.create_backup.password,
|
|
||||||
agentId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _deleteBackup(): Promise<void> {
|
private async _deleteBackup(): Promise<void> {
|
||||||
@@ -459,13 +380,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
--mdc-icon-size: 48px;
|
--mdc-icon-size: 48px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
ha-md-list.summary ha-md-list-item {
|
|
||||||
--md-list-item-supporting-text-size: 1rem;
|
|
||||||
--md-list-item-label-text-size: 0.875rem;
|
|
||||||
|
|
||||||
--md-list-item-label-text-color: var(--secondary-text-color);
|
|
||||||
--md-list-item-supporting-text-color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
.warning {
|
.warning {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
@@ -475,9 +389,6 @@ class HaConfigBackupDetails extends LitElement {
|
|||||||
ha-button.danger {
|
ha-button.danger {
|
||||||
--mdc-theme-primary: var(--error-color);
|
--mdc-theme-primary: var(--error-color);
|
||||||
}
|
}
|
||||||
ha-backup-data-picker {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
ha-md-list-item [slot="supporting-text"] {
|
ha-md-list-item [slot="supporting-text"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -221,8 +221,7 @@ class HaConfigBackupOverview extends LitElement {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-bottom: 24px;
|
margin-bottom: calc(env(safe-area-inset-bottom) + 72px);
|
||||||
margin-bottom: 72px;
|
|
||||||
}
|
}
|
||||||
.card-actions {
|
.card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -50,9 +50,11 @@ class HaConfigBackupSettings extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
public connectedCallback(): void {
|
||||||
super.firstUpdated(_changedProperties);
|
super.connectedCallback();
|
||||||
this._scrollToSection();
|
this._scrollToSection();
|
||||||
|
// Update config the page is displayed (e.g. when coming back from a location detail page)
|
||||||
|
this._config = this.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _scrollToSection() {
|
private async _scrollToSection() {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
|||||||
settings: {
|
settings: {
|
||||||
tag: "ha-config-backup-settings",
|
tag: "ha-config-backup-settings",
|
||||||
load: () => import("./ha-config-backup-settings"),
|
load: () => import("./ha-config-backup-settings"),
|
||||||
|
cache: true,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
tag: "ha-config-backup-location",
|
tag: "ha-config-backup-location",
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import type { LitElement } from "lit";
|
import type { LitElement } from "lit";
|
||||||
|
import { getSignedPath } from "../../../../data/auth";
|
||||||
|
import type { BackupConfig, BackupContent } from "../../../../data/backup";
|
||||||
import {
|
import {
|
||||||
canDecryptBackupOnDownload,
|
canDecryptBackupOnDownload,
|
||||||
getBackupDownloadUrl,
|
getBackupDownloadUrl,
|
||||||
getPreferredAgentForDownload,
|
getPreferredAgentForDownload,
|
||||||
type BackupContent,
|
|
||||||
} from "../../../../data/backup";
|
} from "../../../../data/backup";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
|
||||||
showAlertDialog,
|
|
||||||
showConfirmationDialog,
|
|
||||||
showPromptDialog,
|
|
||||||
} from "../../../lovelace/custom-card-helpers";
|
|
||||||
import { getSignedPath } from "../../../../data/auth";
|
|
||||||
import { fileDownload } from "../../../../util/file_download";
|
import { fileDownload } from "../../../../util/file_download";
|
||||||
|
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||||
|
import { showDownloadDecryptedBackupDialog } from "../dialogs/show-dialog-download-decrypted-backup";
|
||||||
|
|
||||||
const triggerDownload = async (
|
export const downloadBackupFile = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
backupId: string,
|
backupId: string,
|
||||||
preferedAgent: string,
|
preferedAgent: string,
|
||||||
@@ -27,83 +24,51 @@ const triggerDownload = async (
|
|||||||
fileDownload(signedUrl.path);
|
fileDownload(signedUrl.path);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadEncryptedBackup = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
element: LitElement,
|
|
||||||
backup: BackupContent,
|
|
||||||
agentId?: string
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
await showConfirmationDialog(element, {
|
|
||||||
title: "Encryption key incorrect",
|
|
||||||
text: hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.download.incorrect_entered_encryption_key"
|
|
||||||
),
|
|
||||||
confirmText: "Download encrypted",
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
const agentIds = Object.keys(backup.agents);
|
|
||||||
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
|
||||||
|
|
||||||
triggerDownload(hass, backup.backup_id, preferedAgent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestEncryptionKey = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
element: LitElement,
|
|
||||||
backup: BackupContent,
|
|
||||||
agentId?: string
|
|
||||||
): Promise<void> => {
|
|
||||||
const encryptionKey = await showPromptDialog(element, {
|
|
||||||
title: hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
|
||||||
),
|
|
||||||
text: hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.download.incorrect_current_encryption_key"
|
|
||||||
),
|
|
||||||
inputLabel: hass.localize(
|
|
||||||
"ui.panel.config.backup.dialogs.show_encryption_key.title"
|
|
||||||
),
|
|
||||||
inputType: "password",
|
|
||||||
confirmText: hass.localize("ui.common.download"),
|
|
||||||
});
|
|
||||||
if (encryptionKey === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
downloadBackup(hass, element, backup, encryptionKey, agentId, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const downloadBackup = async (
|
export const downloadBackup = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
element: LitElement,
|
element: LitElement,
|
||||||
backup: BackupContent,
|
backup: BackupContent,
|
||||||
encryptionKey?: string | null,
|
backupConfig?: BackupConfig,
|
||||||
agentId?: string,
|
agentId?: string
|
||||||
userProvided = false
|
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const agentIds = Object.keys(backup.agents);
|
const agentIds = Object.keys(backup.agents);
|
||||||
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
const preferedAgent = agentId ?? getPreferredAgentForDownload(agentIds);
|
||||||
const isProtected = backup.agents[preferedAgent]?.protected;
|
const isProtected = backup.agents[preferedAgent]?.protected;
|
||||||
|
|
||||||
if (isProtected) {
|
if (!isProtected) {
|
||||||
if (encryptionKey) {
|
downloadBackupFile(hass, backup.backup_id, preferedAgent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionKey = backupConfig?.create_backup?.password;
|
||||||
|
|
||||||
|
if (!encryptionKey) {
|
||||||
|
showDownloadDecryptedBackupDialog(element, {
|
||||||
|
backup,
|
||||||
|
agentId: preferedAgent,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if we can decrypt it
|
||||||
await canDecryptBackupOnDownload(
|
await canDecryptBackupOnDownload(
|
||||||
hass,
|
hass,
|
||||||
backup.backup_id,
|
backup.backup_id,
|
||||||
preferedAgent,
|
preferedAgent,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
);
|
);
|
||||||
|
downloadBackupFile(hass, backup.backup_id, preferedAgent, encryptionKey);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// If encryption key is incorrect, ask for encryption key
|
||||||
if (err?.code === "password_incorrect") {
|
if (err?.code === "password_incorrect") {
|
||||||
if (userProvided) {
|
showDownloadDecryptedBackupDialog(element, {
|
||||||
downloadEncryptedBackup(hass, element, backup, agentId);
|
backup,
|
||||||
} else {
|
agentId: preferedAgent,
|
||||||
requestEncryptionKey(hass, element, backup, agentId);
|
});
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If decryption is not supported, ask for confirmation and download it encrypted
|
||||||
if (err?.code === "decrypt_not_supported") {
|
if (err?.code === "decrypt_not_supported") {
|
||||||
showAlertDialog(element, {
|
showAlertDialog(element, {
|
||||||
title: hass.localize(
|
title: hass.localize(
|
||||||
@@ -113,13 +78,13 @@ export const downloadBackup = async (
|
|||||||
"ui.panel.config.backup.dialogs.download.decryption_unsupported"
|
"ui.panel.config.backup.dialogs.download.decryption_unsupported"
|
||||||
),
|
),
|
||||||
confirm() {
|
confirm() {
|
||||||
triggerDownload(hass, backup.backup_id, preferedAgent);
|
downloadBackupFile(hass, backup.backup_id, preferedAgent);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
encryptionKey = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Else, show generic error
|
||||||
showAlertDialog(element, {
|
showAlertDialog(element, {
|
||||||
title: hass.localize(
|
title: hass.localize(
|
||||||
"ui.panel.config.backup.dialogs.download.error_check_title",
|
"ui.panel.config.backup.dialogs.download.error_check_title",
|
||||||
@@ -134,13 +99,5 @@ export const downloadBackup = async (
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
requestEncryptionKey(hass, element, backup, agentId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await triggerDownload(hass, backup.backup_id, preferedAgent, encryptionKey);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
|
import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
|
|
||||||
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
import { formatDateTime } from "../../../../common/datetime/format_date_time";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { debounce } from "../../../../common/util/debounce";
|
import { debounce } from "../../../../common/util/debounce";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-tip";
|
|
||||||
import "../../../../components/ha-list-item";
|
|
||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-list-item";
|
||||||
|
import "../../../../components/ha-tip";
|
||||||
import type {
|
import type {
|
||||||
CloudStatusLoggedIn,
|
CloudStatusLoggedIn,
|
||||||
SubscriptionInfo,
|
SubscriptionInfo,
|
||||||
@@ -32,6 +32,7 @@ import "./cloud-ice-servers-pref";
|
|||||||
import "./cloud-remote-pref";
|
import "./cloud-remote-pref";
|
||||||
import "./cloud-tts-pref";
|
import "./cloud-tts-pref";
|
||||||
import "./cloud-webhooks";
|
import "./cloud-webhooks";
|
||||||
|
import { showSupportPackageDialog } from "./show-dialog-cloud-support-package";
|
||||||
|
|
||||||
@customElement("cloud-account")
|
@customElement("cloud-account")
|
||||||
export class CloudAccount extends SubscribeMixin(LitElement) {
|
export class CloudAccount extends SubscribeMixin(LitElement) {
|
||||||
@@ -52,7 +53,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
header="Home Assistant Cloud"
|
header="Home Assistant Cloud"
|
||||||
>
|
>
|
||||||
<ha-button-menu slot="toolbar-icon" @action=${this._deleteCloudData}>
|
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
@@ -65,6 +66,12 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
|||||||
)}
|
)}
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.download_support_package"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section .isWide=${this.isWide}>
|
||||||
@@ -286,6 +293,16 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
|||||||
fireEvent(this, "ha-refresh-cloud-status");
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleMenuAction(ev) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._deleteCloudData();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._downloadSupportPackage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _deleteCloudData() {
|
private async _deleteCloudData() {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
@@ -316,6 +333,10 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _downloadSupportPackage() {
|
||||||
|
showSupportPackageDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
|||||||
206
src/panels/config/cloud/account/dialog-cloud-support-package.ts
Normal file
206
src/panels/config/cloud/account/dialog-cloud-support-package.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-alert";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-circular-progress";
|
||||||
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import "../../../../components/ha-markdown-element";
|
||||||
|
import "../../../../components/ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||||
|
import "../../../../components/ha-select";
|
||||||
|
import "../../../../components/ha-textarea";
|
||||||
|
import { fetchSupportPackage } from "../../../../data/cloud";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { fileDownload } from "../../../../util/file_download";
|
||||||
|
|
||||||
|
@customElement("dialog-cloud-support-package")
|
||||||
|
export class DialogSupportPackage extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _open = false;
|
||||||
|
|
||||||
|
@state() private _supportPackage?: string;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
|
public showDialog() {
|
||||||
|
this._open = true;
|
||||||
|
this._loadSupportPackage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dialogClosed(): void {
|
||||||
|
this._open = false;
|
||||||
|
this._supportPackage = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._dialog?.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._open) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||||
|
<ha-dialog-header slot="headline">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title">Download support package</span>
|
||||||
|
</ha-dialog-header>
|
||||||
|
|
||||||
|
<div slot="content">
|
||||||
|
${this._supportPackage
|
||||||
|
? html`<ha-markdown-element
|
||||||
|
.content=${this._supportPackage}
|
||||||
|
breaks
|
||||||
|
></ha-markdown-element>`
|
||||||
|
: html`
|
||||||
|
<div class="progress-container">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
Generating preview...
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<div class="footer" slot="actions">
|
||||||
|
<ha-alert>
|
||||||
|
This file may contain personal data about your home. Avoid sharing
|
||||||
|
them with unverified or untrusted parties.
|
||||||
|
</ha-alert>
|
||||||
|
<hr />
|
||||||
|
<div class="actions">
|
||||||
|
<ha-button @click=${this.closeDialog}>Close</ha-button>
|
||||||
|
<ha-button @click=${this._download}>Download</ha-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-md-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadSupportPackage() {
|
||||||
|
this._supportPackage = await fetchSupportPackage(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _download() {
|
||||||
|
fileDownload(
|
||||||
|
"data:text/plain;charset=utf-8," +
|
||||||
|
encodeURIComponent(this._supportPackage || ""),
|
||||||
|
"support-package.md"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-md-dialog {
|
||||||
|
min-width: 90vw;
|
||||||
|
min-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: calc(90vh - 260px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-md-dialog {
|
||||||
|
min-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.progress-container {
|
||||||
|
height: calc(100vh - 260px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--divider-color);
|
||||||
|
width: calc(100% + 48px);
|
||||||
|
margin-right: -24px;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
table,
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
display: table;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:nth-child(odd) {
|
||||||
|
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr > td {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr {
|
||||||
|
-webkit-transition: background-color 0.25s ease;
|
||||||
|
transition: background-color 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
table > tbody > tr:hover {
|
||||||
|
background-color: rgba(var(--rgb-primary-text-color), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 15px 5px;
|
||||||
|
display: table-cell;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
padding: 16px 24px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-cloud-support-package": DialogSupportPackage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export const loadSupportPackageDialog = () =>
|
||||||
|
import("./dialog-cloud-support-package");
|
||||||
|
|
||||||
|
export const showSupportPackageDialog = (element: HTMLElement): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-cloud-support-package",
|
||||||
|
dialogImport: loadSupportPackageDialog,
|
||||||
|
dialogParams: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import "@material/mwc-list/mwc-list";
|
import "@material/mwc-list/mwc-list";
|
||||||
import { mdiDeleteForever, mdiDotsVertical } from "@mdi/js";
|
import { mdiDeleteForever, mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@@ -27,6 +27,7 @@ import "../../../../layouts/hass-subpage";
|
|||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../../ha-config-section";
|
import "../../ha-config-section";
|
||||||
|
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
|
||||||
|
|
||||||
@customElement("cloud-login")
|
@customElement("cloud-login")
|
||||||
export class CloudLogin extends LitElement {
|
export class CloudLogin extends LitElement {
|
||||||
@@ -57,7 +58,7 @@ export class CloudLogin extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
header="Home Assistant Cloud"
|
header="Home Assistant Cloud"
|
||||||
>
|
>
|
||||||
<ha-button-menu slot="toolbar-icon" @action=${this._deleteCloudData}>
|
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
@@ -70,6 +71,12 @@ export class CloudLogin extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
|
||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.download_support_package"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
</ha-button-menu>
|
</ha-button-menu>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ha-config-section .isWide=${this.isWide}>
|
<ha-config-section .isWide=${this.isWide}>
|
||||||
@@ -348,6 +355,16 @@ export class CloudLogin extends LitElement {
|
|||||||
fireEvent(this, "flash-message-changed", { value: "" });
|
fireEvent(this, "flash-message-changed", { value: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handleMenuAction(ev) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._deleteCloudData();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._downloadSupportPackage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _deleteCloudData() {
|
private async _deleteCloudData() {
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
@@ -377,6 +394,10 @@ export class CloudLogin extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _downloadSupportPackage() {
|
||||||
|
showSupportPackageDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>${hass.localize("ui.panel.config.tips.join_x")}</a
|
>${hass.localize("ui.panel.config.tips.join_x")}</a
|
||||||
>`,
|
>`,
|
||||||
|
mastodon: html`<a
|
||||||
|
href=${documentationUrl(hass, `/mastodon`)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${hass.localize("ui.panel.config.tips.join_mastodon")}</a
|
||||||
|
>`,
|
||||||
|
bluesky: html`<a
|
||||||
|
href=${documentationUrl(hass, `/bluesky`)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${hass.localize("ui.panel.config.tips.join_bluesky")}</a
|
||||||
|
>`,
|
||||||
discord: html`<a
|
discord: html`<a
|
||||||
href=${documentationUrl(hass, `/join-chat`)}
|
href=${documentationUrl(hass, `/join-chat`)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -1073,7 +1073,14 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
(ent) => computeDomain(ent.entity_id) === "assist_satellite"
|
(ent) => computeDomain(ent.entity_id) === "assist_satellite"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const domains = this._integrations(
|
||||||
|
device,
|
||||||
|
this.entries,
|
||||||
|
this.manifests
|
||||||
|
).map((int) => int.domain);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!domains.includes("voip") &&
|
||||||
assistSatellite &&
|
assistSatellite &&
|
||||||
assistSatelliteSupportsSetupFlow(
|
assistSatelliteSupportsSetupFlow(
|
||||||
this.hass.states[assistSatellite.entity_id]
|
this.hass.states[assistSatellite.entity_id]
|
||||||
@@ -1088,12 +1095,6 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const domains = this._integrations(
|
|
||||||
device,
|
|
||||||
this.entries,
|
|
||||||
this.manifests
|
|
||||||
).map((int) => int.domain);
|
|
||||||
|
|
||||||
if (domains.includes("mqtt")) {
|
if (domains.includes("mqtt")) {
|
||||||
const mqtt = await import(
|
const mqtt = await import(
|
||||||
"./device-detail/integration-elements/mqtt/device-actions"
|
"./device-detail/integration-elements/mqtt/device-actions"
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ import "../../../components/ha-icon-button";
|
|||||||
import "../../../components/ha-md-menu-item";
|
import "../../../components/ha-md-menu-item";
|
||||||
import "../../../components/ha-sub-menu";
|
import "../../../components/ha-sub-menu";
|
||||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { ConfigEntry } from "../../../data/config_entries";
|
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||||
import { sortConfigEntries } from "../../../data/config_entries";
|
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||||
import {
|
import {
|
||||||
@@ -108,6 +108,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ attribute: false }) public entries!: ConfigEntry[];
|
@property({ attribute: false }) public entries!: ConfigEntry[];
|
||||||
|
|
||||||
|
@state() private _subEntries?: SubEntry[];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
entities!: EntityRegistryEntry[];
|
entities!: EntityRegistryEntry[];
|
||||||
@@ -219,6 +221,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
private _setFiltersFromUrl() {
|
private _setFiltersFromUrl() {
|
||||||
const domain = this._searchParms.get("domain");
|
const domain = this._searchParms.get("domain");
|
||||||
const configEntry = this._searchParms.get("config_entry");
|
const configEntry = this._searchParms.get("config_entry");
|
||||||
|
const subEntry = this._searchParms.get("sub_entry");
|
||||||
const label = this._searchParms.has("label");
|
const label = this._searchParms.has("label");
|
||||||
|
|
||||||
if (!domain && !configEntry && !label) {
|
if (!domain && !configEntry && !label) {
|
||||||
@@ -243,6 +246,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
value: configEntry ? [configEntry] : [],
|
value: configEntry ? [configEntry] : [],
|
||||||
items: undefined,
|
items: undefined,
|
||||||
},
|
},
|
||||||
|
sub_entry: {
|
||||||
|
value: subEntry ? [subEntry] : [],
|
||||||
|
items: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this._filterLabel();
|
this._filterLabel();
|
||||||
}
|
}
|
||||||
@@ -334,6 +341,32 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
if (configEntries.length === 1) {
|
if (configEntries.length === 1) {
|
||||||
filteredConfigEntry = configEntries[0];
|
filteredConfigEntry = configEntries[0];
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
key === "sub_entry" &&
|
||||||
|
Array.isArray(filter.value) &&
|
||||||
|
filter.value.length
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
Array.isArray(this._filters.config_entry?.value) &&
|
||||||
|
this._filters.config_entry.value.length === 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const configEntryId = this._filters.config_entry.value[0];
|
||||||
|
outputDevices = outputDevices.filter(
|
||||||
|
(device) =>
|
||||||
|
device.config_entries_subentries[configEntryId] &&
|
||||||
|
(filter.value as string[]).some((subEntryId) =>
|
||||||
|
device.config_entries_subentries[configEntryId].includes(
|
||||||
|
subEntryId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!this._subEntries) {
|
||||||
|
this._loadSubEntries(configEntryId);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
key === "ha-filter-integrations" &&
|
key === "ha-filter-integrations" &&
|
||||||
Array.isArray(filter.value) &&
|
Array.isArray(filter.value) &&
|
||||||
@@ -626,7 +659,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
(area) =>
|
(area) =>
|
||||||
html`<ha-md-menu-item
|
html`<ha-md-menu-item
|
||||||
.value=${area.area_id}
|
.value=${area.area_id}
|
||||||
@click=${this._handleBulkArea}
|
.clickAction=${this._handleBulkArea}
|
||||||
>
|
>
|
||||||
${area.icon
|
${area.icon
|
||||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||||
@@ -637,7 +670,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="headline">${area.name}</div>
|
<div slot="headline">${area.name}</div>
|
||||||
</ha-md-menu-item>`
|
</ha-md-menu-item>`
|
||||||
)}
|
)}
|
||||||
<ha-md-menu-item .value=${null} @click=${this._handleBulkArea}>
|
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||||
@@ -645,7 +678,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
</ha-md-menu-item>
|
</ha-md-menu-item>
|
||||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||||
<ha-md-menu-item @click=${this._bulkCreateArea}>
|
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||||
@@ -684,7 +717,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
</ha-md-menu-item>`;
|
</ha-md-menu-item>`;
|
||||||
})}
|
})}
|
||||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||||
<ha-md-menu-item @click=${this._bulkCreateLabel}>
|
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||||
</div></ha-md-menu-item
|
</div></ha-md-menu-item
|
||||||
@@ -755,7 +788,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
${this.entries?.find(
|
${this.entries?.find(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
entry.entry_id === this._filters.config_entry!.value![0]
|
entry.entry_id === this._filters.config_entry!.value![0]
|
||||||
)?.title || this._filters.config_entry.value[0]}
|
)?.title || this._filters.config_entry.value[0]}${this._filters
|
||||||
|
.config_entry.value.length === 1 &&
|
||||||
|
Array.isArray(this._filters.sub_entry?.value) &&
|
||||||
|
this._filters.sub_entry.value.length
|
||||||
|
? html` (${this._subEntries?.find(
|
||||||
|
(entry) =>
|
||||||
|
entry.subentry_id === this._filters.sub_entry!.value![0]
|
||||||
|
)?.title || this._filters.sub_entry!.value![0]})`
|
||||||
|
: nothing}
|
||||||
</ha-alert>`
|
</ha-alert>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<ha-filter-floor-areas
|
<ha-filter-floor-areas
|
||||||
@@ -888,6 +929,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _loadSubEntries(entryId: string) {
|
||||||
|
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||||
|
}
|
||||||
|
|
||||||
private _filterExpanded(ev) {
|
private _filterExpanded(ev) {
|
||||||
if (ev.detail.expanded) {
|
if (ev.detail.expanded) {
|
||||||
this._expandedFilter = ev.target.localName;
|
this._expandedFilter = ev.target.localName;
|
||||||
@@ -969,10 +1014,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
this._selected = ev.detail.value;
|
this._selected = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleBulkArea(ev) {
|
private _handleBulkArea = (item) => {
|
||||||
const area = ev.currentTarget.value;
|
const area = item.value;
|
||||||
this._bulkAddArea(area);
|
this._bulkAddArea(area);
|
||||||
}
|
};
|
||||||
|
|
||||||
private async _bulkAddArea(area: string) {
|
private async _bulkAddArea(area: string) {
|
||||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||||
@@ -999,7 +1044,7 @@ ${rejected
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _bulkCreateArea() {
|
private _bulkCreateArea = () => {
|
||||||
showAreaRegistryDetailDialog(this, {
|
showAreaRegistryDetailDialog(this, {
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
const area = await createAreaRegistryEntry(this.hass, values);
|
const area = await createAreaRegistryEntry(this.hass, values);
|
||||||
@@ -1007,7 +1052,7 @@ ${rejected
|
|||||||
return area;
|
return area;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private async _handleBulkLabel(ev) {
|
private async _handleBulkLabel(ev) {
|
||||||
const label = ev.currentTarget.value;
|
const label = ev.currentTarget.value;
|
||||||
@@ -1045,7 +1090,7 @@ ${rejected
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _bulkCreateLabel() {
|
private _bulkCreateLabel = () => {
|
||||||
showLabelDetailDialog(this, {
|
showLabelDetailDialog(this, {
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
const label = await createLabelRegistryEntry(this.hass, values);
|
const label = await createLabelRegistryEntry(this.hass, values);
|
||||||
@@ -1053,7 +1098,7 @@ ${rejected
|
|||||||
return label;
|
return label;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private _handleSortingChanged(ev: CustomEvent) {
|
private _handleSortingChanged(ev: CustomEvent) {
|
||||||
this._activeSorting = ev.detail;
|
this._activeSorting = ev.detail;
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ import "../../../components/ha-icon-button";
|
|||||||
import "../../../components/ha-md-menu-item";
|
import "../../../components/ha-md-menu-item";
|
||||||
import "../../../components/ha-sub-menu";
|
import "../../../components/ha-sub-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { ConfigEntry } from "../../../data/config_entries";
|
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||||
import { getConfigEntries } from "../../../data/config_entries";
|
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import type {
|
import type {
|
||||||
DataTableFiltersItems,
|
DataTableFiltersItems,
|
||||||
@@ -146,6 +146,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _entries?: ConfigEntry[];
|
@state() private _entries?: ConfigEntry[];
|
||||||
|
|
||||||
|
@state() private _subEntries?: SubEntry[];
|
||||||
|
|
||||||
@state() private _manifests?: IntegrationManifest[];
|
@state() private _manifests?: IntegrationManifest[];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@@ -522,6 +524,27 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||||||
if (configEntries.length === 1) {
|
if (configEntries.length === 1) {
|
||||||
filteredConfigEntry = configEntries[0];
|
filteredConfigEntry = configEntries[0];
|
||||||
}
|
}
|
||||||
|
} else if (
|
||||||
|
key === "sub_entry" &&
|
||||||
|
Array.isArray(filter) &&
|
||||||
|
filter.length
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
Array.isArray(this._filters.config_entry) &&
|
||||||
|
this._filters.config_entry.length === 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filteredEntities = filteredEntities.filter(
|
||||||
|
(entity) =>
|
||||||
|
entity.config_subentry_id &&
|
||||||
|
(filter as string[]).includes(entity.config_subentry_id)
|
||||||
|
);
|
||||||
|
if (!this._subEntries) {
|
||||||
|
this._loadSubEntries(this._filters.config_entry[0]);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
key === "ha-filter-integrations" &&
|
key === "ha-filter-integrations" &&
|
||||||
Array.isArray(filter) &&
|
Array.isArray(filter) &&
|
||||||
@@ -904,14 +927,22 @@ ${
|
|||||||
</ha-md-button-menu>
|
</ha-md-button-menu>
|
||||||
${
|
${
|
||||||
Array.isArray(this._filters.config_entry) &&
|
Array.isArray(this._filters.config_entry) &&
|
||||||
this._filters.config_entry?.length
|
this._filters.config_entry.length
|
||||||
? html`<ha-alert slot="filter-pane">
|
? html`<ha-alert slot="filter-pane">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
"ui.panel.config.entities.picker.filtering_by_config_entry"
|
||||||
)}
|
)}
|
||||||
${this._entries?.find(
|
${this._entries?.find(
|
||||||
(entry) => entry.entry_id === this._filters.config_entry![0]
|
(entry) => entry.entry_id === this._filters.config_entry![0]
|
||||||
)?.title || this._filters.config_entry[0]}
|
)?.title || this._filters.config_entry[0]}${this._filters
|
||||||
|
.config_entry.length === 1 &&
|
||||||
|
Array.isArray(this._filters.sub_entry) &&
|
||||||
|
this._filters.sub_entry.length
|
||||||
|
? html` (${this._subEntries?.find(
|
||||||
|
(entry) =>
|
||||||
|
entry.subentry_id === this._filters.sub_entry![0]
|
||||||
|
)?.title || this._filters.sub_entry[0]})`
|
||||||
|
: nothing}
|
||||||
</ha-alert>`
|
</ha-alert>`
|
||||||
: nothing
|
: nothing
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1055,7 @@ ${
|
|||||||
private _setFiltersFromUrl() {
|
private _setFiltersFromUrl() {
|
||||||
const domain = this._searchParms.get("domain");
|
const domain = this._searchParms.get("domain");
|
||||||
const configEntry = this._searchParms.get("config_entry");
|
const configEntry = this._searchParms.get("config_entry");
|
||||||
|
const subEntry = this._searchParms.get("sub_entry");
|
||||||
const label = this._searchParms.has("label");
|
const label = this._searchParms.has("label");
|
||||||
|
|
||||||
if (!domain && !configEntry && !label) {
|
if (!domain && !configEntry && !label) {
|
||||||
@@ -1036,6 +1068,7 @@ ${
|
|||||||
"ha-filter-states": [],
|
"ha-filter-states": [],
|
||||||
"ha-filter-integrations": domain ? [domain] : [],
|
"ha-filter-integrations": domain ? [domain] : [],
|
||||||
config_entry: configEntry ? [configEntry] : [],
|
config_entry: configEntry ? [configEntry] : [],
|
||||||
|
sub_entry: subEntry ? [subEntry] : [],
|
||||||
};
|
};
|
||||||
this._filterLabel();
|
this._filterLabel();
|
||||||
}
|
}
|
||||||
@@ -1093,6 +1126,7 @@ ${
|
|||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
area_id: null,
|
area_id: null,
|
||||||
config_entry_id: null,
|
config_entry_id: null,
|
||||||
|
config_subentry_id: null,
|
||||||
device_id: null,
|
device_id: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
@@ -1384,6 +1418,10 @@ ${rejected
|
|||||||
this._entries = await getConfigEntries(this.hass);
|
this._entries = await getConfigEntries(this.hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _loadSubEntries(entryId: string) {
|
||||||
|
this._subEntries = await getSubEntries(this.hass, entryId);
|
||||||
|
}
|
||||||
|
|
||||||
private _addDevice() {
|
private _addDevice() {
|
||||||
const { filteredConfigEntry, filteredDomains } =
|
const { filteredConfigEntry, filteredDomains } =
|
||||||
this._filteredEntitiesAndDomains(
|
this._filteredEntitiesAndDomains(
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import { hardwareBrandsUrl } from "../../../util/brands-url";
|
|||||||
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
import type { ECOption } from "../../../resources/echarts";
|
import type { ECOption } from "../../../resources/echarts";
|
||||||
import { getTimeAxisLabelConfig } from "../../../components/chart/axis-label";
|
|
||||||
|
|
||||||
const DATASAMPLES = 60;
|
const DATASAMPLES = 60;
|
||||||
|
|
||||||
@@ -153,13 +152,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
|
|||||||
this._chartOptions = {
|
this._chartOptions = {
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "time",
|
type: "time",
|
||||||
axisLabel: getTimeAxisLabelConfig(this.hass.locale, this.hass.config),
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
(category) =>
|
(category) =>
|
||||||
html`<ha-md-menu-item
|
html`<ha-md-menu-item
|
||||||
.value=${category.category_id}
|
.value=${category.category_id}
|
||||||
@click=${this._handleBulkCategory}
|
.clickAction=${this._handleBulkCategory}
|
||||||
>
|
>
|
||||||
${category.icon
|
${category.icon
|
||||||
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
|
||||||
@@ -569,7 +569,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="headline">${category.name}</div>
|
<div slot="headline">${category.name}</div>
|
||||||
</ha-md-menu-item>`
|
</ha-md-menu-item>`
|
||||||
)}
|
)}
|
||||||
<ha-md-menu-item .value=${null} @click=${this._handleBulkCategory}>
|
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
"ui.panel.config.automation.picker.bulk_actions.no_category"
|
||||||
@@ -577,7 +577,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
</ha-md-menu-item>
|
</ha-md-menu-item>
|
||||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||||
<ha-md-menu-item @click=${this._bulkCreateCategory}>
|
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||||
</div>
|
</div>
|
||||||
@@ -612,7 +612,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
</ha-label>
|
</ha-label>
|
||||||
</ha-md-menu-item> `;
|
</ha-md-menu-item> `;
|
||||||
})}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
})}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||||
<ha-md-menu-item @click=${this._bulkCreateLabel}>
|
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||||
</div>
|
</div>
|
||||||
@@ -958,10 +958,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleBulkCategory(ev) {
|
private _handleBulkCategory = (item) => {
|
||||||
const category = ev.currentTarget.value;
|
const category = item.value;
|
||||||
this._bulkAddCategory(category);
|
this._bulkAddCategory(category);
|
||||||
}
|
};
|
||||||
|
|
||||||
private async _bulkAddCategory(category: string) {
|
private async _bulkAddCategory(category: string) {
|
||||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||||
@@ -1234,7 +1234,7 @@ ${rejected
|
|||||||
showHelperDetailDialog(this, {});
|
showHelperDetailDialog(this, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _bulkCreateCategory() {
|
private _bulkCreateCategory = () => {
|
||||||
showCategoryRegistryDetailDialog(this, {
|
showCategoryRegistryDetailDialog(this, {
|
||||||
scope: "helpers",
|
scope: "helpers",
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
@@ -1247,9 +1247,9 @@ ${rejected
|
|||||||
return category;
|
return category;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private _bulkCreateLabel() {
|
private _bulkCreateLabel = () => {
|
||||||
showLabelDetailDialog(this, {
|
showLabelDetailDialog(this, {
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
const label = await createLabelRegistryEntry(this.hass, values);
|
const label = await createLabelRegistryEntry(this.hass, values);
|
||||||
@@ -1257,7 +1257,7 @@ ${rejected
|
|||||||
return label;
|
return label;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
private _handleSortingChanged(ev: CustomEvent) {
|
private _handleSortingChanged(ev: CustomEvent) {
|
||||||
this._activeSorting = ev.detail;
|
this._activeSorting = ev.detail;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { LocalizeFunc } from "../../../common/translations/localize";
|
|||||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-icon-button-prev";
|
import "../../../components/ha-icon-button-prev";
|
||||||
import "../../../components/search-input";
|
import "../../../components/search-input";
|
||||||
|
import { getConfigEntries } from "../../../data/config_entries";
|
||||||
import { fetchConfigFlowInProgress } from "../../../data/config_flow";
|
import { fetchConfigFlowInProgress } from "../../../data/config_flow";
|
||||||
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
|
||||||
import {
|
import {
|
||||||
@@ -49,9 +50,6 @@ import "./ha-domain-integrations";
|
|||||||
import "./ha-integration-list-item";
|
import "./ha-integration-list-item";
|
||||||
import type { AddIntegrationDialogParams } from "./show-add-integration-dialog";
|
import type { AddIntegrationDialogParams } from "./show-add-integration-dialog";
|
||||||
import { showYamlIntegrationDialog } from "./show-add-integration-dialog";
|
import { showYamlIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
import { getConfigEntries } from "../../../data/config_entries";
|
|
||||||
import { stripDiacritics } from "../../../common/string/strip-diacritics";
|
|
||||||
import { getStripDiacriticsFn } from "../../../util/fuse";
|
|
||||||
|
|
||||||
export interface IntegrationListItem {
|
export interface IntegrationListItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -256,7 +254,7 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
isCaseSensitive: false,
|
isCaseSensitive: false,
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
minMatchCharLength: Math.min(filter.length, 2),
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
getFn: getStripDiacriticsFn,
|
ignoreDiacritics: true,
|
||||||
};
|
};
|
||||||
const helpers = Object.entries(h).map(([domain, integration]) => ({
|
const helpers = Object.entries(h).map(([domain, integration]) => ({
|
||||||
domain,
|
domain,
|
||||||
@@ -266,16 +264,15 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
is_built_in: integration.is_built_in !== false,
|
is_built_in: integration.is_built_in !== false,
|
||||||
cloud: integration.iot_class?.startsWith("cloud_"),
|
cloud: integration.iot_class?.startsWith("cloud_"),
|
||||||
}));
|
}));
|
||||||
const normalizedFilter = stripDiacritics(filter);
|
|
||||||
return [
|
return [
|
||||||
...new Fuse(integrations, options)
|
...new Fuse(integrations, options)
|
||||||
.search(normalizedFilter)
|
.search(filter)
|
||||||
.map((result) => result.item),
|
.map((result) => result.item),
|
||||||
...new Fuse(yamlIntegrations, options)
|
...new Fuse(yamlIntegrations, options)
|
||||||
.search(normalizedFilter)
|
.search(filter)
|
||||||
.map((result) => result.item),
|
.map((result) => result.item),
|
||||||
...new Fuse(helpers, options)
|
...new Fuse(helpers, options)
|
||||||
.search(normalizedFilter)
|
.search(filter)
|
||||||
.map((result) => result.item),
|
.map((result) => result.item),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -657,6 +654,7 @@ class AddIntegrationDialog extends LitElement {
|
|||||||
startFlowHandler: domain,
|
startFlowHandler: domain,
|
||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
manifest,
|
manifest,
|
||||||
|
navigateToResult: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export class HaConfigFlowCard extends LitElement {
|
|||||||
}
|
}
|
||||||
showConfigFlowDialog(this, {
|
showConfigFlowDialog(this, {
|
||||||
continueFlowId: this.flow.flow_id,
|
continueFlowId: this.flow.flow_id,
|
||||||
|
navigateToResult: true,
|
||||||
dialogClosedCallback: () => {
|
dialogClosedCallback: () => {
|
||||||
this._handleFlowUpdated();
|
this._handleFlowUpdated();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
mdiOpenInNew,
|
mdiOpenInNew,
|
||||||
mdiPackageVariant,
|
mdiPackageVariant,
|
||||||
mdiPlayCircleOutline,
|
mdiPlayCircleOutline,
|
||||||
|
mdiPlus,
|
||||||
mdiProgressHelper,
|
mdiProgressHelper,
|
||||||
mdiReload,
|
mdiReload,
|
||||||
mdiReloadAlert,
|
mdiReloadAlert,
|
||||||
@@ -52,14 +53,17 @@ import { getSignedPath } from "../../../data/auth";
|
|||||||
import type {
|
import type {
|
||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
DisableConfigEntryResult,
|
DisableConfigEntryResult,
|
||||||
|
SubEntry,
|
||||||
} from "../../../data/config_entries";
|
} from "../../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
ERROR_STATES,
|
ERROR_STATES,
|
||||||
RECOVERABLE_STATES,
|
RECOVERABLE_STATES,
|
||||||
deleteConfigEntry,
|
deleteConfigEntry,
|
||||||
|
deleteSubEntry,
|
||||||
disableConfigEntry,
|
disableConfigEntry,
|
||||||
enableConfigEntry,
|
enableConfigEntry,
|
||||||
getConfigEntries,
|
getConfigEntries,
|
||||||
|
getSubEntries,
|
||||||
reloadConfigEntry,
|
reloadConfigEntry,
|
||||||
updateConfigEntry,
|
updateConfigEntry,
|
||||||
} from "../../../data/config_entries";
|
} from "../../../data/config_entries";
|
||||||
@@ -106,6 +110,7 @@ import { fileDownload } from "../../../util/file_download";
|
|||||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
|
||||||
|
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||||
|
|
||||||
export const renderConfigEntryError = (
|
export const renderConfigEntryError = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -172,6 +177,8 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _domainEntities: Record<string, string[]> = {};
|
@state() private _domainEntities: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
@state() private _subEntries: Record<string, SubEntry[]> = {};
|
||||||
|
|
||||||
private _configPanel = memoizeOne(
|
private _configPanel = memoizeOne(
|
||||||
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
|
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
|
||||||
Object.values(panels).find(
|
Object.values(panels).find(
|
||||||
@@ -214,11 +221,18 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (changedProperties.has("domain")) {
|
if (changedProperties.has("domain")) {
|
||||||
this.hass.loadBackendTranslation("title", [this.domain]);
|
this.hass.loadBackendTranslation("title", [this.domain]);
|
||||||
|
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
|
||||||
this._extraConfigEntries = undefined;
|
this._extraConfigEntries = undefined;
|
||||||
this._fetchManifest();
|
this._fetchManifest();
|
||||||
this._fetchDiagnostics();
|
this._fetchDiagnostics();
|
||||||
this._fetchEntitySources();
|
this._fetchEntitySources();
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
changedProperties.has("configEntries") ||
|
||||||
|
changedProperties.has("_extraConfigEntries")
|
||||||
|
) {
|
||||||
|
this._fetchSubEntries();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchEntitySources() {
|
private async _fetchEntitySources() {
|
||||||
@@ -673,6 +687,73 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
ev.target.style.display = "none";
|
ev.target.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderDeviceLine(
|
||||||
|
item: ConfigEntry,
|
||||||
|
devices: DeviceRegistryEntry[],
|
||||||
|
services: DeviceRegistryEntry[],
|
||||||
|
entities: EntityRegistryEntry[],
|
||||||
|
subItem?: SubEntry
|
||||||
|
) {
|
||||||
|
let devicesLine: (TemplateResult | string)[] = [];
|
||||||
|
for (const [items, localizeKey] of [
|
||||||
|
[devices, "devices"],
|
||||||
|
[services, "services"],
|
||||||
|
] as const) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url =
|
||||||
|
items.length === 1
|
||||||
|
? `/config/devices/device/${items[0].id}`
|
||||||
|
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`;
|
||||||
|
devicesLine.push(
|
||||||
|
// no white space before/after template on purpose
|
||||||
|
html`<a href=${url}
|
||||||
|
>${this.hass.localize(
|
||||||
|
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
||||||
|
{ count: items.length }
|
||||||
|
)}</a
|
||||||
|
>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entities.length) {
|
||||||
|
devicesLine.push(
|
||||||
|
// no white space before/after template on purpose
|
||||||
|
html`<a
|
||||||
|
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}${subItem ? `&sub_entry=${subItem.subentry_id}` : ""}`}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.entities",
|
||||||
|
{ count: entities.length }
|
||||||
|
)}</a
|
||||||
|
>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devicesLine.length === 0) {
|
||||||
|
devicesLine = [
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.no_devices_or_entities"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else if (devicesLine.length === 2) {
|
||||||
|
devicesLine = [
|
||||||
|
devicesLine[0],
|
||||||
|
` ${this.hass.localize("ui.common.and")} `,
|
||||||
|
devicesLine[1],
|
||||||
|
];
|
||||||
|
} else if (devicesLine.length === 3) {
|
||||||
|
devicesLine = [
|
||||||
|
devicesLine[0],
|
||||||
|
", ",
|
||||||
|
devicesLine[1],
|
||||||
|
` ${this.hass.localize("ui.common.and")} `,
|
||||||
|
devicesLine[2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return devicesLine;
|
||||||
|
}
|
||||||
|
|
||||||
private _renderConfigEntry(item: ConfigEntry) {
|
private _renderConfigEntry(item: ConfigEntry) {
|
||||||
let stateText: Parameters<typeof this.hass.localize> | undefined;
|
let stateText: Parameters<typeof this.hass.localize> | undefined;
|
||||||
let stateTextExtra: TemplateResult | string | undefined;
|
let stateTextExtra: TemplateResult | string | undefined;
|
||||||
@@ -720,66 +801,13 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
)}.`);
|
)}.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [items, localizeKey] of [
|
devicesLine = this._renderDeviceLine(item, devices, services, entities);
|
||||||
[devices, "devices"],
|
|
||||||
[services, "services"],
|
|
||||||
] as const) {
|
|
||||||
if (items.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const url =
|
|
||||||
items.length === 1
|
|
||||||
? `/config/devices/device/${items[0].id}`
|
|
||||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`;
|
|
||||||
devicesLine.push(
|
|
||||||
// no white space before/after template on purpose
|
|
||||||
html`<a href=${url}
|
|
||||||
>${this.hass.localize(
|
|
||||||
`ui.panel.config.integrations.config_entry.${localizeKey}`,
|
|
||||||
{ count: items.length }
|
|
||||||
)}</a
|
|
||||||
>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entities.length) {
|
|
||||||
devicesLine.push(
|
|
||||||
// no white space before/after template on purpose
|
|
||||||
html`<a
|
|
||||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.entities",
|
|
||||||
{ count: entities.length }
|
|
||||||
)}</a
|
|
||||||
>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesLine.length === 0) {
|
|
||||||
devicesLine = [
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.no_devices_or_entities"
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else if (devicesLine.length === 2) {
|
|
||||||
devicesLine = [
|
|
||||||
devicesLine[0],
|
|
||||||
` ${this.hass.localize("ui.common.and")} `,
|
|
||||||
devicesLine[1],
|
|
||||||
];
|
|
||||||
} else if (devicesLine.length === 3) {
|
|
||||||
devicesLine = [
|
|
||||||
devicesLine[0],
|
|
||||||
", ",
|
|
||||||
devicesLine[1],
|
|
||||||
` ${this.hass.localize("ui.common.and")} `,
|
|
||||||
devicesLine[2],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPanel = this._configPanel(item.domain, this.hass.panels);
|
const configPanel = this._configPanel(item.domain, this.hass.panels);
|
||||||
|
|
||||||
|
const subEntries = this._subEntries[item.entry_id] || [];
|
||||||
|
|
||||||
return html`<ha-md-list-item
|
return html`<ha-md-list-item
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
config_entry: true,
|
config_entry: true,
|
||||||
@@ -913,6 +941,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
)}
|
)}
|
||||||
</ha-md-menu-item>
|
</ha-md-menu-item>
|
||||||
|
|
||||||
|
${Object.keys(item.supported_subentry_types).map(
|
||||||
|
(flowType) =>
|
||||||
|
html`<ha-md-menu-item
|
||||||
|
@click=${this._addSubEntry}
|
||||||
|
.entry=${item}
|
||||||
|
.flowType=${flowType}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
`component.${item.domain}.config_subentries.${flowType}.title`
|
||||||
|
)}</ha-md-menu-item
|
||||||
|
>`
|
||||||
|
)}
|
||||||
|
|
||||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
|
||||||
|
|
||||||
${this._diagnosticHandler && item.state === "loaded"
|
${this._diagnosticHandler && item.state === "loaded"
|
||||||
@@ -989,6 +1032,69 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-md-button-menu>
|
</ha-md-button-menu>
|
||||||
|
</ha-md-list-item>
|
||||||
|
${subEntries.map((subEntry) => this._renderSubEntry(item, subEntry))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderSubEntry(configEntry: ConfigEntry, subEntry: SubEntry) {
|
||||||
|
const devices = this._getConfigEntryDevices(configEntry).filter((device) =>
|
||||||
|
device.config_entries_subentries[configEntry.entry_id]?.includes(
|
||||||
|
subEntry.subentry_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const services = this._getConfigEntryServices(configEntry).filter(
|
||||||
|
(device) =>
|
||||||
|
device.config_entries_subentries[configEntry.entry_id]?.includes(
|
||||||
|
subEntry.subentry_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const entities = this._getConfigEntryEntities(configEntry).filter(
|
||||||
|
(entity) => entity.config_subentry_id === subEntry.subentry_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`<ha-md-list-item
|
||||||
|
class="sub-entry"
|
||||||
|
data-entry-id=${configEntry.entry_id}
|
||||||
|
.configEntry=${configEntry}
|
||||||
|
.subEntry=${subEntry}
|
||||||
|
>
|
||||||
|
<span slot="headline">${subEntry.title}</span>
|
||||||
|
<span slot="supporting-text"
|
||||||
|
>${this._renderDeviceLine(
|
||||||
|
configEntry,
|
||||||
|
devices,
|
||||||
|
services,
|
||||||
|
entities,
|
||||||
|
subEntry
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
${configEntry.supported_subentry_types[subEntry.subentry_type]
|
||||||
|
?.supports_reconfigure
|
||||||
|
? html`
|
||||||
|
<ha-button slot="end" @click=${this._handleReconfigureSub}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.configure"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-md-button-menu positioning="popover" slot="end">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-md-menu-item class="warning" @click=${this._handleDeleteSub}>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
class="warning"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.delete"
|
||||||
|
)}
|
||||||
|
</ha-md-menu-item>
|
||||||
|
</ha-md-button-menu>
|
||||||
</ha-md-list-item>`;
|
</ha-md-list-item>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,6 +1115,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
private _continueFlow(ev) {
|
private _continueFlow(ev) {
|
||||||
showConfigFlowDialog(this, {
|
showConfigFlowDialog(this, {
|
||||||
continueFlowId: ev.target.flow.flow_id,
|
continueFlowId: ev.target.flow.flow_id,
|
||||||
|
navigateToResult: true,
|
||||||
dialogClosedCallback: () => {
|
dialogClosedCallback: () => {
|
||||||
// this._handleFlowUpdated();
|
// this._handleFlowUpdated();
|
||||||
},
|
},
|
||||||
@@ -1030,6 +1137,27 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _fetchSubEntries() {
|
||||||
|
const subEntriesPromises = (
|
||||||
|
this._extraConfigEntries || this.configEntries
|
||||||
|
)?.map((entry) =>
|
||||||
|
entry.num_subentries
|
||||||
|
? getSubEntries(this.hass, entry.entry_id).then((subEntries) => ({
|
||||||
|
entry_id: entry.entry_id,
|
||||||
|
subEntries,
|
||||||
|
}))
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
if (subEntriesPromises) {
|
||||||
|
const subEntries = await Promise.all(subEntriesPromises);
|
||||||
|
this._subEntries = {};
|
||||||
|
subEntries.forEach((entry) => {
|
||||||
|
if (!entry) return;
|
||||||
|
this._subEntries[entry.entry_id] = entry.subEntries;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _fetchDiagnostics() {
|
private async _fetchDiagnostics() {
|
||||||
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
|
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
|
||||||
return;
|
return;
|
||||||
@@ -1177,6 +1305,49 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _handleReconfigureSub(ev: Event): Promise<void> {
|
||||||
|
const configEntry = (
|
||||||
|
(ev.target as HTMLElement).closest(".sub-entry") as any
|
||||||
|
).configEntry;
|
||||||
|
const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
|
||||||
|
.subEntry;
|
||||||
|
|
||||||
|
showSubConfigFlowDialog(
|
||||||
|
this,
|
||||||
|
configEntry,
|
||||||
|
subEntry.flowType || subEntry.subentry_type,
|
||||||
|
{
|
||||||
|
startFlowHandler: configEntry.entry_id,
|
||||||
|
subEntryId: subEntry.subentry_id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleDeleteSub(ev: Event): Promise<void> {
|
||||||
|
const configEntry = (
|
||||||
|
(ev.target as HTMLElement).closest(".sub-entry") as any
|
||||||
|
).configEntry;
|
||||||
|
const subEntry = ((ev.target as HTMLElement).closest(".sub-entry") as any)
|
||||||
|
.subEntry;
|
||||||
|
const confirmed = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||||
|
{ title: subEntry.title }
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.delete_confirm_text"
|
||||||
|
),
|
||||||
|
confirmText: this.hass!.localize("ui.common.delete"),
|
||||||
|
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await deleteSubEntry(this.hass, configEntry.entry_id, subEntry.subentry_id);
|
||||||
|
}
|
||||||
|
|
||||||
private _handleDisable(ev: Event): void {
|
private _handleDisable(ev: Event): void {
|
||||||
this._disableIntegration(
|
this._disableIntegration(
|
||||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||||
@@ -1384,6 +1555,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
showAdvanced: this.hass.userData?.showAdvanced,
|
showAdvanced: this.hass.userData?.showAdvanced,
|
||||||
manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
|
manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
|
||||||
entryId: configEntry.entry_id,
|
entryId: configEntry.entry_id,
|
||||||
|
navigateToResult: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1454,6 +1626,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _addSubEntry(ev) {
|
||||||
|
showSubConfigFlowDialog(this, ev.target.entry, ev.target.flowType, {
|
||||||
|
startFlowHandler: ev.target.entry.entry_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
@@ -1583,6 +1761,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
ha-md-list-item.sub-entry {
|
||||||
|
--md-list-item-leading-space: 50px;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user