Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
0e3f2dc017 Set climate as toggle domain 2025-02-10 14:59:31 +01:00
123 changed files with 2033 additions and 4954 deletions

View File

@@ -5,7 +5,7 @@
"context": ".."
},
"appPort": "8124:8123",
"postCreateCommand": "./.devcontainer/post_create.sh",
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"DEV_CONTAINER": "1",

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# This script will run after the container is created
# add github cli
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
# Update package lists
sudo apt-get update
sudo apt upgrade -y
# Install necessary packages
sudo apt-get install -y libpcap-dev gh
# Display a message
echo "Post-create script has been executed successfully."

42
.vscode/tasks.json vendored
View File

@@ -1,42 +1,6 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Develop and serve Frontend",
"type": "shell",
"command": "script/develop_and_serve -c ${input:coreUrl}",
// Sync changes here to other tasks until issue resolved
// https://github.com/Microsoft/vscode/issues/61497
"problemMatcher": {
"owner": "ha-build",
"source": "ha-build",
"fileLocation": "absolute",
"severity": "error",
"pattern": [
{
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
"severity": 1,
"file": 2,
"message": 3,
"line": 4,
"column": 5
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "Changes detected. Starting compilation",
"endsPattern": "Build done @"
}
},
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
},
"runOptions": {
"instanceLimit": 1
}
},
{
"label": "Develop Frontend",
"type": "gulp",
@@ -277,12 +241,6 @@
"id": "supervisorToken",
"type": "promptString",
"description": "The token for the Remote API proxy add-on"
},
{
"id": "coreUrl",
"type": "promptString",
"description": "The URL of the Home Assistant Core instance",
"default": "http://127.0.0.1:8123"
}
]
}

View File

@@ -1,16 +1,16 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
export default [
...rootConfig,
{
rules: {
"no-console": "off",
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
"@typescript-eslint/no-require-imports": "off",
"prefer-arrow-callback": "off",
},
},
});
];

View File

@@ -90,14 +90,6 @@ function copyMapPanel(staticDir) {
npmPath("leaflet/dist/leaflet.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.css"),
staticPath("images/leaflet/")
);
copyFileDir(
npmPath("leaflet.markercluster/dist/MarkerCluster.Default.css"),
staticPath("images/leaflet/")
);
fs.copySync(
npmPath("leaflet/dist/images"),
staticPath("images/leaflet/images/")

View File

@@ -65,7 +65,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,
@@ -86,7 +85,6 @@ export class HaDemo extends HomeAssistantAppEl {
},
{
config_entry_id: "co2signal",
config_subentry_id: null,
device_id: "co2signal",
area_id: null,
disabled_by: null,

View File

@@ -11,7 +11,6 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,

View File

@@ -1,16 +1,11 @@
// @ts-check
/* eslint-disable import/no-extraneous-dependencies */
import unusedImports from "eslint-plugin-unused-imports";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -20,14 +15,17 @@ const compat = new FlatCompat({
allConfig: js.configs.all,
});
export default tseslint.config(
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
export default [
...compat.extends(
"airbnb-base",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/strict",
"plugin:@typescript-eslint/stylistic",
"plugin:wc/recommended",
"plugin:lit/all",
"plugin:lit-a11y/recommended",
"prettier"
),
{
plugins: {
"unused-imports": unusedImports,
@@ -45,7 +43,7 @@ export default tseslint.config(
Polymer: true,
},
parser: tseslint.parser,
parser: tsParser,
ecmaVersion: 2020,
sourceType: "module",
@@ -186,5 +184,5 @@ export default tseslint.config(
],
"no-use-before-define": "off",
},
}
);
},
];

View File

@@ -1,10 +1,10 @@
// @ts-check
import tseslint from "typescript-eslint";
import rootConfig from "../eslint.config.mjs";
export default tseslint.config(...rootConfig, {
rules: {
"no-console": "off",
export default [
...rootConfig,
{
rules: {
"no-console": "off",
},
},
});
];

View File

@@ -48,7 +48,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -72,7 +71,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -96,7 +94,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,

View File

@@ -47,7 +47,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "bedroom",
configuration_url: null,
config_entries: ["config_entry_1"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -71,7 +70,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: "backyard",
configuration_url: null,
config_entries: ["config_entry_2"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,
@@ -95,7 +93,6 @@ const DEVICES: DeviceRegistryEntry[] = [
area_id: null,
configuration_url: null,
config_entries: ["config_entry_3"],
config_entries_subentries: {},
connections: [],
disabled_by: null,
entry_type: null,

View File

@@ -32,8 +32,6 @@ const createConfigEntry = (
supports_remove_device: false,
supports_unload: true,
supports_reconfigure: true,
supported_subentry_types: {},
num_subentries: 0,
disabled_by: null,
pref_disable_new_entities: false,
pref_disable_polling: false,
@@ -190,7 +188,6 @@ const createEntityRegistryEntries = (
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
config_subentry_id: null,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
@@ -217,7 +214,6 @@ const createDeviceRegistryEntries = (
{
entry_type: null,
config_entries: [item.entry_id],
config_entries_subentries: {},
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",

View File

@@ -14,7 +14,7 @@ import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"hassio-backup-uploaded": { backup: HassioBackup };
"backup-uploaded": { backup: HassioBackup };
"backup-cleared": undefined;
}
}
@@ -70,7 +70,7 @@ export class HassioUploadBackup extends LitElement {
this._uploading = true;
try {
const backup = await uploadBackup(this.hass, file);
fireEvent(this, "hassio-backup-uploaded", { backup: backup.data });
fireEvent(this, "backup-uploaded", { backup: backup.data });
} catch (err: any) {
showAlertDialog(this, {
title: "Upload failed",

View File

@@ -5,6 +5,7 @@ import { customElement, property, query } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
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-formfield";
import "../../../src/components/ha-textfield";
@@ -18,10 +19,13 @@ import type {
} from "../../../src/data/hassio/backup";
import type { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../../../src/types";
import type { HomeAssistant, TranslationDict } from "../../../src/types";
import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
interface CheckboxItem {
slug: string;
checked: boolean;
@@ -63,6 +67,8 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public backup?: HassioBackupDetail;
@@ -109,6 +115,10 @@ export class SupervisorBackupContent extends LitElement {
this._focusTarget?.focus();
}
private _localize = (key: BackupOrRestoreKey) =>
this.supervisor?.localize(`backup.${key}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${key}`);
protected render() {
if (!this.onboarding && !this.supervisor) {
return nothing;
@@ -122,8 +132,8 @@ export class SupervisorBackupContent extends LitElement {
${this.backup
? html`<div class="details">
${this.backup.type === "full"
? this.supervisor?.localize("backup.full_backup")
: this.supervisor?.localize("backup.partial_backup")}
? this._localize("full_backup")
: this._localize("partial_backup")}
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
${this.hass
? formatDateTime(
@@ -135,7 +145,7 @@ export class SupervisorBackupContent extends LitElement {
</div>`
: html`<ha-textfield
name="backupName"
.label=${this.supervisor?.localize("backup.name")}
.label=${this._localize("name")}
.value=${this.backupName}
@change=${this._handleTextValueChanged}
>
@@ -143,13 +153,11 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup || this.backup.type === "full"
? html`<div class="sub-header">
${!this.backup
? this.supervisor?.localize("backup.type")
: this.supervisor?.localize("backup.select_type")}
? this._localize("type")
: this._localize("select_type")}
</div>
<div class="backup-types">
<ha-formfield
.label=${this.supervisor?.localize("backup.full_backup")}
>
<ha-formfield .label=${this._localize("full_backup")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
@@ -158,9 +166,7 @@ export class SupervisorBackupContent extends LitElement {
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor?.localize("backup.partial_backup")}
>
<ha-formfield .label=${this._localize("partial_backup")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
@@ -196,7 +202,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.folders")}
.label=${this._localize("folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
@@ -216,7 +222,7 @@ export class SupervisorBackupContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor?.localize("backup.addons")}
.label=${this._localize("addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
@@ -241,7 +247,7 @@ export class SupervisorBackupContent extends LitElement {
${!this.backup
? html`<ha-formfield
class="password"
.label=${this.supervisor?.localize("backup.password_protection")}
.label=${this._localize("password_protection")}
>
<ha-checkbox
.checked=${this.backupHasPassword}
@@ -253,7 +259,7 @@ export class SupervisorBackupContent extends LitElement {
${this.backupHasPassword
? html`
<ha-password-field
.label=${this.supervisor?.localize("backup.password")}
.label=${this._localize("password")}
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
@@ -261,7 +267,7 @@ export class SupervisorBackupContent extends LitElement {
</ha-password-field>
${!this.backup
? html`<ha-password-field
.label=${this.supervisor?.localize("backup.confirm_password")}
.label=${this._localize("confirm_password")}
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}

View File

@@ -72,7 +72,7 @@ export class DialogHassioBackupUpload
</ha-header-bar>
</div>
<hassio-upload-backup
@hassio-backup-uploaded=${this._backupUploaded}
@backup-uploaded=${this._backupUploaded}
.hass=${this.hass}
></hassio-upload-backup>
</ha-dialog>

View File

@@ -35,6 +35,7 @@ import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-backup-content";
import type { SupervisorBackupContent } from "../../components/supervisor-backup-content";
import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup";
import type { BackupOrRestoreKey } from "../../util/translations";
import type { HaMdDialog } from "../../../../src/components/ha-md-dialog";
@customElement("dialog-hassio-backup")
@@ -42,7 +43,7 @@ class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _error?: string;
@@ -61,13 +62,9 @@ class HassioBackupDialog
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._dialogParams.supervisor?.localize(
"backup.no_backup_found"
);
this._error = this._localize("no_backup_found");
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._dialogParams.supervisor?.localize(
"backup.restore_no_home_assistant"
);
this._error = this._localize("restore_no_home_assistant");
}
this._restoringBackup = false;
}
@@ -85,6 +82,13 @@ class HassioBackupDialog
return true;
}
private _localize(key: BackupOrRestoreKey) {
return (
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
this._dialogParams!.localize!(`ui.panel.page-onboarding.restore.${key}`)
);
}
protected render() {
if (!this._dialogParams || !this._backup) {
return nothing;
@@ -98,7 +102,7 @@ class HassioBackupDialog
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._dialogParams.supervisor?.localize("backup.close")}
.label=${this._localize("close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
@@ -146,6 +150,7 @@ class HassioBackupDialog
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>
@@ -156,7 +161,7 @@ class HassioBackupDialog
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._dialogParams.supervisor?.localize("backup.restore")}
${this._localize("restore")}
</ha-button>
</div>
</ha-md-dialog>
@@ -191,22 +196,18 @@ class HassioBackupDialog
}
if (
!(await showConfirmationDialog(this, {
title: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
}`
title: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
),
text: supervisor?.localize(
`backup.${
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
}`
text: this._localize(
this._backup!.type === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
),
confirmText: supervisor?.localize("backup.restore"),
dismissText: supervisor?.localize("backup.cancel"),
confirmText: this._localize("restore"),
dismissText: this._localize("cancel"),
}))
) {
this._restoringBackup = false;
@@ -226,8 +227,7 @@ class HassioBackupDialog
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message ||
supervisor?.localize("backup.restore_start_failed");
error?.body?.message || this._localize("restore_start_failed");
} finally {
this._restoringBackup = false;
}
@@ -286,7 +286,7 @@ class HassioBackupDialog
title: supervisor.localize("backup.remote_download_title"),
text: supervisor.localize("backup.remote_download_text"),
confirmText: supervisor.localize("backup.download"),
dismissText: supervisor?.localize("backup.cancel"),
dismissText: this._localize("cancel"),
});
if (!confirm) {
return;
@@ -302,7 +302,7 @@ class HassioBackupDialog
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || "";
: this._localize("unnamed_backup");
}
static get styles(): CSSResultGroup {

View File

@@ -1,4 +1,5 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
@@ -7,6 +8,7 @@ export interface HassioBackupDialogParams {
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;
}
export const showHassioBackupDialog = (

View File

@@ -0,0 +1,4 @@
import type { TranslationDict } from "../../../src/types";
export type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];

View File

@@ -26,25 +26,25 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.9",
"@babel/runtime": "7.26.7",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/commands": "6.8.0",
"@codemirror/language": "6.10.8",
"@codemirror/legacy-modes": "6.4.3",
"@codemirror/search": "6.5.9",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.2",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.3",
"@formatjs/intl-displaynames": "6.8.10",
"@formatjs/intl-durationformat": "0.7.3",
"@formatjs/intl-datetimeformat": "6.17.2",
"@formatjs/intl-displaynames": "6.8.9",
"@formatjs/intl-durationformat": "0.7.2",
"@formatjs/intl-getcanonicallocales": "2.5.4",
"@formatjs/intl-listformat": "7.7.10",
"@formatjs/intl-locale": "4.2.10",
"@formatjs/intl-numberformat": "8.15.3",
"@formatjs/intl-pluralrules": "5.4.3",
"@formatjs/intl-relativetimeformat": "11.4.10",
"@formatjs/intl-listformat": "7.7.9",
"@formatjs/intl-locale": "4.2.9",
"@formatjs/intl-numberformat": "8.15.2",
"@formatjs/intl-pluralrules": "5.4.2",
"@formatjs/intl-relativetimeformat": "11.4.9",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -53,9 +53,9 @@
"@fullcalendar/timegrid": "6.1.15",
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
"@lit-labs/virtualizer": "2.1.0",
"@lit-labs/motion": "1.0.7",
"@lit-labs/observers": "2.0.4",
"@lit-labs/virtualizer": "2.0.15",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -91,8 +91,8 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.6.5",
"@vaadin/vaadin-themable-mixin": "24.6.5",
"@vaadin/combo-box": "24.6.3",
"@vaadin/vaadin-themable-mixin": "24.6.3",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.9",
@@ -116,18 +116,16 @@
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.15",
"intl-messageformat": "10.7.14",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.7",
"marked": "15.0.6",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.4",
@@ -139,7 +137,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "2.0.2",
"ua-parser-js": "2.0.1",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",
@@ -154,20 +152,20 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.26.9",
"@babel/core": "7.26.7",
"@babel/helper-define-polyfill-provider": "0.6.3",
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.26.9",
"@babel/preset-env": "7.26.9",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.7",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.18.2",
"@lokalise/node-api": "13.1.0",
"@octokit/auth-oauth-device": "7.1.3",
"@octokit/plugin-retry": "7.1.4",
"@octokit/rest": "21.1.1",
"@lokalise/node-api": "13.0.0",
"@octokit/auth-oauth-device": "7.1.2",
"@octokit/plugin-retry": "7.1.3",
"@octokit/rest": "21.1.0",
"@rsdoctor/rspack-plugin": "0.4.13",
"@rspack/cli": "1.2.3",
"@rspack/core": "1.2.3",
"@rspack/cli": "1.2.2",
"@rspack/core": "1.2.2",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@@ -177,7 +175,6 @@
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
@@ -186,12 +183,14 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "8.23.0",
"@typescript-eslint/parser": "8.23.0",
"@vitest/coverage-v8": "3.0.5",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.20.1",
"eslint": "9.19.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.0.1",
"eslint-import-resolver-webpack": "0.13.10",
@@ -216,8 +215,9 @@
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"object-hash": "3.0.0",
"pinst": "3.0.0",
"prettier": "3.5.1",
"prettier": "3.4.2",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.2",
@@ -225,7 +225,6 @@
"terser-webpack-plugin": "5.3.11",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.3",
"typescript-eslint": "8.24.1",
"vitest": "3.0.5",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
@@ -240,7 +239,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.15.0",
"globals": "15.14.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.6.0"

View File

@@ -136,18 +136,11 @@ export function theme2hex(themeColor: string): string {
}
const rgbFromColorName = colors[themeColor];
if (rgbFromColorName) {
return rgb2hex(rgbFromColorName);
if (!rgbFromColorName) {
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
}
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (rgbMatch) {
const [, r, g, b] = rgbMatch.map(Number);
return rgb2hex([r, g, b]);
}
// We have a named color, and there's nothing in the table,
// so nothing further we can do with it.
// Compare/border/background color will all be the same.
return themeColor;
return rgb2hex(rgbFromColorName);
}

View File

@@ -99,6 +99,7 @@ export const DOMAINS_TOGGLE = new Set([
"switch",
"group",
"automation",
"climate",
"humidifier",
"valve",
]);

View File

@@ -26,20 +26,6 @@ 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
export const formatShortDateTimeWithYear = (
dateObj: Date,

View File

@@ -16,30 +16,11 @@ export const setupLeafletMap = async (
const Leaflet = (await import("leaflet")).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
await import("leaflet.markercluster");
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style);
const markerClusterStyle = document.createElement("link");
markerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.css"
);
markerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(markerClusterStyle);
const defaultMarkerClusterStyle = document.createElement("link");
defaultMarkerClusterStyle.setAttribute(
"href",
"/static/images/leaflet/MarkerCluster.Default.css"
);
defaultMarkerClusterStyle.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(defaultMarkerClusterStyle);
map.setView([52.3731339, 4.8903147], 13);
const tileLayer = createTileLayer(Leaflet).addTo(map);

View File

@@ -1,9 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE_STATES } from "../../data/entity";
import type { HomeAssistant } from "../../types";
import { computeDomain } from "./compute_domain";
import { stringCompare } from "../string/compare";
export const FIXED_DOMAIN_STATES = {
alarm_control_panel: [
@@ -240,7 +237,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
};
export const getStates = (
hass: HomeAssistant,
state: HassEntity,
attribute: string | undefined = undefined
): string[] => {
@@ -273,19 +269,7 @@ export const getStates = (
case "device_tracker":
case "person":
if (!attribute) {
result.push(
...Object.entries(hass.states)
.filter(
([entityId, stateObj]) =>
computeDomain(entityId) === "zone" &&
entityId !== "zone.home" &&
stateObj.attributes.friendly_name
)
.map(([_entityId, stateObj]) => stateObj.attributes.friendly_name!)
.sort((zone1, zone2) =>
stringCompare(zone1, zone2, hass.locale.language)
)
);
result.push("home", "not_home");
}
break;
case "event":

View File

@@ -1,32 +0,0 @@
import type { LatLngExpression, Layer, Map, MarkerOptions } from "leaflet";
import { Marker } from "leaflet";
export class DecoratedMarker extends Marker {
decorationLayer: Layer | undefined;
constructor(
latlng: LatLngExpression,
decorationLayer?: Layer,
options?: MarkerOptions
) {
super(latlng, options);
this.decorationLayer = decorationLayer;
}
onAdd(map: Map) {
super.onAdd(map);
// If decoration has been provided, add it to the map as well
this.decorationLayer?.addTo(map);
return this;
}
onRemove(map: Map) {
// If decoration has been provided, remove it from the map as well
this.decorationLayer?.remove();
return super.onRemove(map);
}
}

View File

@@ -41,7 +41,7 @@ export class HaProgressButton extends LitElement {
indeterminate
></ha-circular-progress>
`
: nothing}
: ""}
</div>
`}
`;
@@ -117,9 +117,6 @@ export class HaProgressButton extends LitElement {
mwc-button.error slot {
visibility: hidden;
}
:host([destructive]) {
--mdc-theme-primary: var(--error-color);
}
`;
}

View File

@@ -24,7 +24,6 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -68,16 +67,12 @@ export class HaChartBase extends LitElement {
private _listeners: (() => void)[] = [];
private _originalZrFlush?: () => void;
public disconnectedCallback() {
super.disconnectedCallback();
while (this._listeners.length) {
this._listeners.pop()!();
}
this.chart?.dispose();
this.chart = undefined;
this._originalZrFlush = undefined;
}
public connectedCallback() {
@@ -88,19 +83,19 @@ export class HaChartBase extends LitElement {
this._listeners.push(
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
if (this._reducedMotion !== matches) {
this._reducedMotion = matches;
this._setChartOptions({ animation: !this._reducedMotion });
}
this._reducedMotion = matches;
this.chart?.setOption({ animation: !this._reducedMotion });
})
);
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
if ((isMac && ev.metaKey) || (!isMac && ev.ctrlKey)) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
}
}
};
@@ -109,7 +104,9 @@ export class HaChartBase extends LitElement {
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
}
}
};
@@ -127,24 +124,27 @@ export class HaChartBase extends LitElement {
}
public willUpdate(changedProps: PropertyValues): void {
if (!this.chart) {
super.willUpdate(changedProps);
if (!this.hasUpdated || !this.chart) {
return;
}
if (changedProps.has("_themes")) {
this._setupChart();
return;
}
let chartOptions: ECOption = {};
if (changedProps.has("data")) {
chartOptions.series = this.data;
this.chart.setOption(
{ series: this.data },
{ lazyUpdate: true, replaceMerge: ["series"] }
);
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
if (Object.keys(chartOptions).length > 0) {
this._setChartOptions(chartOptions);
if (changedProps.has("options") || changedProps.has("_isZoomed")) {
this.chart.setOption(this._createOptions(), {
lazyUpdate: true,
// if we replace the whole object, it will reset the dataZoom
replaceMerge: ["grid"],
});
}
}
@@ -158,6 +158,7 @@ export class HaChartBase extends LitElement {
style=${styleMap({
height: this.height ?? `${this._getDefaultHeight()}px`,
})}
@wheel=${this._handleWheel}
>
<div class="chart"></div>
${this._isZoomed
@@ -239,8 +240,8 @@ export class HaChartBase extends LitElement {
type: "inside",
orient: "horizontal",
filterMode: "none",
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
moveOnMouseMove: this._isZoomed,
preventDefaultMouseMove: this._isZoomed,
zoomLock: !this._isTouchDevice && !this._modifierPressed,
};
}
@@ -511,33 +512,25 @@ export class HaChartBase extends LitElement {
return Math.max(this.clientWidth / 2, 200);
}
private _setChartOptions(options: ECOption) {
if (!this.chart) {
return;
}
if (!this._originalZrFlush) {
const dataSize = ensureArray(this.data).reduce(
(acc, series) => acc + (series.data as any[]).length,
0
);
if (dataSize > 10000) {
// for large datasets zr.flush takes 30-40% of the render time
// so we delay it a bit to avoid blocking the main thread
const zr = this.chart.getZr();
this._originalZrFlush = zr.flush.bind(zr);
zr.flush = () => {
setTimeout(() => {
this._originalZrFlush?.();
}, 10);
};
}
}
const replaceMerge = options.series ? ["series"] : [];
this.chart.setOption(options, { replaceMerge });
}
private _handleZoomReset() {
this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 });
this._modifierPressed = false;
}
private _handleWheel(e: WheelEvent) {
// if the window is not focused, we don't receive the keydown events but scroll still works
if (!this.options?.dataZoom) {
const modifierPressed = (isMac && e.metaKey) || (!isMac && e.ctrlKey);
if (modifierPressed) {
e.preventDefault();
}
if (modifierPressed !== this._modifierPressed) {
this._modifierPressed = modifierPressed;
this.chart?.setOption({
dataZoom: this._getDataZoomConfig(),
});
}
}
}
static styles = css`

View File

@@ -75,8 +75,6 @@ export class StateHistoryChartLine extends LitElement {
@state() private _yWidth = 25;
@state() private _visualMap?: VisualMapComponentOption[];
private _chartTime: Date = new Date();
protected render() {
@@ -94,7 +92,7 @@ export class StateHistoryChartLine extends LitElement {
`;
}
private _renderTooltip = (params: any) => {
private _renderTooltip(params: any) {
const time = params[0].axisValue;
const title =
formatDateTimeWithSeconds(
@@ -117,7 +115,7 @@ export class StateHistoryChartLine extends LitElement {
return;
}
// If the datapoint is not found, we need to find the last datapoint before the current time
let lastData: any;
let lastData;
const data = dataset.data || [];
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
@@ -177,7 +175,7 @@ export class StateHistoryChartLine extends LitElement {
})
.join("<br>")
);
};
}
private _datasetHidden(ev: CustomEvent) {
this._hiddenStats.add(ev.detail.name);
@@ -210,8 +208,8 @@ export class StateHistoryChartLine extends LitElement {
changedProps.has("minYAxis") ||
changedProps.has("maxYAxis") ||
changedProps.has("fitYData") ||
changedProps.has("_chartData") ||
changedProps.has("paddingYAxis") ||
changedProps.has("_visualMap") ||
changedProps.has("_yWidth")
) {
const rtl = computeRTL(this.hass);
@@ -282,11 +280,37 @@ export class StateHistoryChartLine extends LitElement {
right: rtl ? Math.max(this.paddingYAxis, this._yWidth) : 1,
bottom: 30,
},
visualMap: this._visualMap,
visualMap: this._chartData
.map((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return false;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
return {
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
};
})
.filter(Boolean) as VisualMapComponentOption[],
tooltip: {
trigger: "axis",
appendTo: document.body,
formatter: this._renderTooltip,
formatter: this._renderTooltip.bind(this),
},
};
}
@@ -701,33 +725,6 @@ export class StateHistoryChartLine extends LitElement {
this._chartData = datasets;
this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex;
const visualMap: VisualMapComponentOption[] = [];
this._chartData.forEach((_, seriesIndex) => {
const dataIndex = this._datasetToDataIndex[seriesIndex];
const data = this.data[dataIndex];
if (!data.statistics || data.statistics.length === 0) {
return;
}
// render stat data with a slightly transparent line
const firstStateTS =
data.states[0]?.last_changed ?? this.endTime.getTime();
visualMap.push({
show: false,
seriesIndex,
dimension: 0,
pieces: [
{
max: firstStateTS - 0.01,
colorAlpha: 0.5,
},
{
min: firstStateTS,
colorAlpha: 1,
},
],
});
});
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
}
private _clampYAxis(value?: number | ((values: any) => number)) {

View File

@@ -273,13 +273,11 @@ export class StatisticsChart extends LitElement {
this._chartOptions = {
xAxis: [
{
id: "xAxis",
type: "time",
min: startTime,
max: this.endTime,
max: endTime,
},
{
id: "hiddenAxis",
type: "time",
show: false,
},
@@ -370,6 +368,7 @@ export class StatisticsChart extends LitElement {
if (endTime > new Date()) {
endTime = new Date();
}
this.endTime = endTime;
let unit: string | undefined | null;

View File

@@ -448,7 +448,6 @@ export class HaDataTable extends LitElement {
)}
@click=${this._handleHeaderClick}
.columnId=${key}
title=${ifDefined(column.title)}
>
${column.sortable
? html`

View File

@@ -57,7 +57,7 @@ class HaEntityStatePicker extends LitElement {
(this._comboBox as any).items = [
...(this.extraOptions ?? []),
...(this.entityId && stateObj
? getStates(this.hass, stateObj, this.attribute).map((key) => ({
? getStates(stateObj, this.attribute).map((key) => ({
value: key,
label: !this.attribute
? this.hass.formatEntityState(stateObj, key)

View File

@@ -5,16 +5,15 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import {
addDays,
subHours,
endOfDay,
endOfMonth,
endOfWeek,
endOfYear,
isThisYear,
startOfDay,
startOfMonth,
startOfWeek,
startOfYear,
isThisYear,
} from "date-fns";
import { fromZonedTime, toZonedTime } from "date-fns-tz";
import type { PropertyValues, TemplateResult } from "lit";
@@ -179,96 +178,6 @@ export class HaDateRangePicker extends LitElement {
weekStartsOn,
}),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-1h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
1
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-12h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
12
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-24h"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-7d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 7
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.now-30d"
)]: [
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
24 * 30
),
calcDate(
today,
subHours,
this.hass.locale,
this.hass.config,
0
),
],
}
: {}),
};
@@ -486,55 +395,44 @@ export class HaDateRangePicker extends LitElement {
}
static styles = css`
ha-icon-button {
direction: var(--direction);
}
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
ha-icon-button {
direction: var(--direction);
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
.date-range-ranges {
border-right: 1px solid var(--divider-color);
}
.date-range-footer {
display: flex;
justify-content: flex-end;
padding: 8px;
border-top: 1px solid var(--divider-color);
}
ha-textarea {
display: inline-block;
width: 340px;
}
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%;
display: inline-block;
width: 340px;
}
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
@media only screen and (max-height: 940px) and (max-width: 800px) {
.date-range-ranges {
overflow: auto;
max-height: calc(70vh - 330px);
min-height: 160px;
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%
}
:host([header-position]) .date-range-ranges {
max-height: calc(90vh - 430px);
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
}
`;
`;
}
declare global {

View File

@@ -11,7 +11,6 @@ import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
import type { LocalizeFunc } from "../common/translations/localize";
declare global {
interface HASSDomEvents {
@@ -24,8 +23,6 @@ declare global {
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@property() public accept!: string;
@property() public icon?: string;
@@ -34,10 +31,6 @@ export class HaFileUpload extends LitElement {
@property() public secondary?: string;
@property({ attribute: "uploading-label" }) public uploadingLabel?: string;
@property({ attribute: "delete-label" }) public deleteLabel?: string;
@property() public supports?: string;
@property({ type: Object }) public value?: File | File[] | FileList | string;
@@ -80,22 +73,23 @@ export class HaFileUpload extends LitElement {
}
public render(): TemplateResult {
const localize = this.localize || this.hass!.localize;
return html`
${this.uploading
? html`<div class="container">
<div class="uploading">
<span class="header"
>${this.uploadingLabel || this.value
? localize("ui.components.file-upload.uploading_name", {
name: this._name,
})
: localize("ui.components.file-upload.uploading")}</span
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this._name }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
)}</span
>
${this.progress
? html`<div class="progress">
${this.progress}${this.hass &&
blankBeforePercent(this.hass!.locale)}%
${this.progress}${blankBeforePercent(this.hass!.locale)}%
</div>`
: nothing}
</div>
@@ -122,11 +116,14 @@ export class HaFileUpload extends LitElement {
.path=${this.icon || mdiFileUpload}
></ha-svg-icon>
<ha-button unelevated @click=${this._openFilePicker}>
${this.label || localize("ui.components.file-upload.label")}
${this.label ||
this.hass?.localize("ui.components.file-upload.label")}
</ha-button>
<span class="secondary"
>${this.secondary ||
localize("ui.components.file-upload.secondary")}</span
this.hass?.localize(
"ui.components.file-upload.secondary"
)}</span
>
<span class="supports">${this.supports}</span>`
: typeof this.value === "string"
@@ -139,7 +136,8 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.deleteLabel || localize("ui.common.delete")}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -157,8 +155,8 @@ export class HaFileUpload extends LitElement {
</div>
<ha-icon-button
@click=${this._clearValue}
.label=${this.deleteLabel ||
localize("ui.common.delete")}
.label=${this.hass?.localize("ui.common.delete") ||
"Delete"}
.path=${mdiDelete}
></ha-icon-button>
</div>`
@@ -240,10 +238,6 @@ export class HaFileUpload extends LitElement {
border-radius: var(--mdc-shape-small, 4px);
height: 100%;
}
.row {
display: flex;
align-items: center;
}
label.container {
border: dashed 1px
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));

View File

@@ -1,12 +1,7 @@
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import hash from "object-hash";
import { fireEvent } from "../common/dom/fire_event";
import { renderMarkdown } from "../resources/render-markdown";
import { CacheManager } from "../util/cache-manager";
const markdownCache = new CacheManager<string>(1000);
const _gitHubMarkdownAlerts = {
reType:
@@ -31,16 +26,6 @@ class HaMarkdownElement extends ReactiveElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
public disconnectedCallback() {
super.disconnectedCallback();
if (this.cache) {
const key = this._computeCacheKey();
markdownCache.set(key, this.innerHTML);
}
}
protected createRenderRoot() {
return this;
}
@@ -52,24 +37,6 @@ class HaMarkdownElement extends ReactiveElement {
}
}
protected willUpdate(_changedProperties: PropertyValues): void {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
this.innerHTML = markdownCache.get(key)!;
this._resize();
}
}
}
private _computeCacheKey() {
return hash({
content: this.content,
allowSvg: this.allowSvg,
breaks: this.breaks,
});
}
private async _render() {
this.innerHTML = await renderMarkdown(
String(this.content),

View File

@@ -13,8 +13,6 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean, attribute: "lazy-images" }) public lazyImages =
false;
@property({ type: Boolean }) public cache = false;
protected render() {
if (!this.content) {
return nothing;
@@ -25,7 +23,6 @@ export class HaMarkdown extends LitElement {
.allowSvg=${this.allowSvg}
.breaks=${this.breaks}
.lazyImages=${this.lazyImages}
.cache=${this.cache}
></ha-markdown-element>`;
}

View File

@@ -8,10 +8,9 @@ import type {
Map,
Marker,
Polyline,
MarkerClusterGroup,
} from "leaflet";
import type { PropertyValues } from "lit";
import { css, ReactiveElement } from "lit";
import { ReactiveElement, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatDateTime } from "../../common/datetime/format_date_time";
@@ -27,7 +26,6 @@ import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
import "./ha-entity-marker";
import { DecoratedMarker } from "../../common/map/decorated_marker";
declare global {
// for fire event
@@ -86,9 +84,6 @@ export class HaMap extends ReactiveElement {
@property({ type: Number }) public zoom = 14;
@property({ attribute: "cluster-markers", type: Boolean })
public clusterMarkers = true;
@state() private _loaded = false;
public leafletMap?: Map;
@@ -101,12 +96,10 @@ export class HaMap extends ReactiveElement {
private _mapFocusItems: (Marker | Circle)[] = [];
private _mapZones: DecoratedMarker[] = [];
private _mapZones: (Marker | Circle)[] = [];
private _mapFocusZones: (Marker | Circle)[] = [];
private _mapCluster: MarkerClusterGroup | undefined;
private _mapPaths: (Polyline | CircleMarker)[] = [];
private _clickCount = 0;
@@ -158,10 +151,6 @@ export class HaMap extends ReactiveElement {
}
}
if (changedProps.has("clusterMarkers")) {
this._drawEntities();
}
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
}
@@ -186,7 +175,6 @@ export class HaMap extends ReactiveElement {
) {
return;
}
this._updateMapStyle();
}
@@ -438,11 +426,6 @@ export class HaMap extends ReactiveElement {
this._mapFocusZones = [];
}
if (this._mapCluster) {
this._mapCluster.remove();
this._mapCluster = undefined;
}
if (!this.entities) {
return;
}
@@ -498,24 +481,26 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
}
// create marker with the icon
this._mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
})
);
// create circle around it
const circle = Leaflet.circle([latitude, longitude], {
interactive: false,
color: passive ? passiveZoneColor : zoneColor,
radius,
});
const marker = new DecoratedMarker([latitude, longitude], circle, {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className,
}),
interactive: this.interactiveZones,
title,
});
this._mapZones.push(marker);
this._mapZones.push(circle);
if (
this.fitZones &&
(typeof entity === "string" || entity.focus !== false)
@@ -553,7 +538,7 @@ export class HaMap extends ReactiveElement {
}
// create marker with the icon
const marker = new DecoratedMarker([latitude, longitude], undefined, {
const marker = Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: entityMarker,
iconSize: [48, 48],
@@ -561,33 +546,24 @@ export class HaMap extends ReactiveElement {
}),
title: title,
});
this._mapItems.push(marker);
if (typeof entity === "string" || entity.focus !== false) {
this._mapFocusItems.push(marker);
}
// create circle around if entity has accuracy
if (gpsAccuracy) {
marker.decorationLayer = Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
});
this._mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: darkPrimaryColor,
radius: gpsAccuracy,
})
);
}
this._mapItems.push(marker);
}
if (this.clusterMarkers) {
this._mapCluster = Leaflet.markerClusterGroup({
showCoverageOnHover: false,
removeOutsideVisibleBounds: false,
});
this._mapCluster.addLayers(this._mapItems);
map.addLayer(this._mapCluster);
} else {
this._mapItems.forEach((marker) => map.addLayer(marker));
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
}

View File

@@ -1,81 +1,25 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement } from "lit/decorators";
import "../ha-icon";
import "../ha-svg-icon";
import { classMap } from "lit/directives/class-map";
export type TileIconImageStyle = "square" | "rounded-square" | "circle";
export const DEFAULT_TILE_ICON_BORDER_STYLE = "circle";
@customElement("ha-tile-icon")
export class HaTileIcon extends LitElement {
@property({ type: Boolean, reflect: true })
public interactive = false;
@property({ attribute: "border-style", type: String })
public imageStyle?: TileIconImageStyle;
@property({ attribute: false })
public imageUrl?: string;
protected render(): TemplateResult {
if (this.imageUrl) {
const imageStyle = this.imageStyle || DEFAULT_TILE_ICON_BORDER_STYLE;
return html`
<div class="container ${classMap({ [imageStyle]: this.imageUrl })}">
<img alt="" src=${this.imageUrl} />
</div>
<slot></slot>
`;
}
return html`
<div class="container ${this.interactive ? "background" : ""}">
<slot name="icon"></slot>
<div class="shape">
<slot></slot>
</div>
<slot></slot>
`;
}
static styles = css`
:host {
--tile-icon-color: var(--disabled-color);
--tile-icon-opacity: 0.2;
--tile-icon-hover-opacity: 0.35;
--mdc-icon-size: 24px;
position: relative;
user-select: none;
transition: transform 180ms ease-in-out;
--mdc-icon-size: 22px;
}
:host([interactive]:active) {
transform: scale(1.2);
}
:host([interactive]:hover) {
--tile-icon-opacity: var(--tile-icon-hover-opacity);
}
.container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 18px;
overflow: hidden;
transition: box-shadow 180ms ease-in-out;
}
:host([interactive]:focus-visible) .container {
box-shadow: 0 0 0 2px var(--tile-icon-color);
}
.container.rounded-square {
border-radius: 8px;
}
.container.square {
border-radius: 0;
}
.container.background::before {
.shape::before {
content: "";
position: absolute;
top: 0;
@@ -83,21 +27,24 @@ export class HaTileIcon extends LitElement {
height: 100%;
width: 100%;
background-color: var(--tile-icon-color);
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--tile-icon-opacity);
transition: background-color 180ms ease-in-out;
opacity: 0.2;
}
.container ::slotted([slot="icon"]) {
.shape {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: color 180ms ease-in-out;
overflow: hidden;
}
.shape ::slotted(*) {
display: flex;
color: var(--tile-icon-color);
transition: color 180ms ease-in-out;
pointer-events: none;
}
.container img {
width: 100%;
height: 100%;
object-fit: cover;
}
`;
}

View File

@@ -0,0 +1,53 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
export type TileImageStyle = "square" | "rounded-square" | "circle";
@customElement("ha-tile-image")
export class HaTileImage extends LitElement {
@property({ attribute: false }) public imageUrl?: string;
@property({ attribute: false }) public imageAlt?: string;
@property({ attribute: false }) public imageStyle: TileImageStyle = "circle";
protected render() {
return html`
<div class="image ${this.imageStyle}">
${this.imageUrl
? html`<img alt=${ifDefined(this.imageAlt)} src=${this.imageUrl} />`
: nothing}
</div>
`;
}
static styles = css`
.image {
position: relative;
width: 36px;
height: 36px;
border-radius: 18px;
display: flex;
flex: none;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image.rounded-square {
border-radius: 8%;
}
.image.square {
border-radius: 0;
}
.image img {
width: 100%;
height: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tile-image": HaTileImage;
}
}

View File

@@ -2,6 +2,7 @@ import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import checkValidDate from "../common/datetime/check_valid_date";
import {
formatDateTime,
formatDateTimeNumeric,
@@ -12,9 +13,6 @@ import type { HomeAssistant } from "../types";
import { fileDownload } from "../util/file_download";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
import checkValidDate from "../common/datetime/check_valid_date";
import { handleFetchPromise } from "../util/hass-call-api";
export const enum BackupScheduleRecurrence {
NEVER = "never",
@@ -131,13 +129,7 @@ export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
agent_errors: Record<string, string>;
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
last_non_idle_event: ManagerStateEvent | null;
next_automatic_backup: string | null;
next_automatic_backup_additional: boolean;
state: BackupManagerState;
backing_up: boolean;
}
export interface BackupDetails {
@@ -239,23 +231,27 @@ export const restoreBackup = (
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
agentIds: string[]
): Promise<{ backup_id: string }> => {
agent_ids: string[]
): Promise<void> => {
const fd = new FormData();
fd.append("file", file);
const params = new URLSearchParams();
const params = agent_ids.reduce((acc, agent_id) => {
acc.append("agent_id", agent_id);
return acc;
}, new URLSearchParams());
agentIds.forEach((agentId) => {
params.append("agent_id", agentId);
});
return handleFetchPromise(
hass.fetchWithAuth(`/api/backup/upload?${params.toString()}`, {
const resp = await hass.fetchWithAuth(
`/api/backup/upload?${params.toString()}`,
{
method: "POST",
body: fd,
})
}
);
if (!resp.ok) {
throw new Error(`${resp.status} ${resp.statusText}`);
}
};
export const getPreferredAgentForDownload = (agents: string[]) => {
@@ -453,13 +449,3 @@ export const getFormattedBackupTime = memoizeOne(
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,
};

View File

@@ -1,66 +0,0 @@
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,
})
);
};

View File

@@ -19,8 +19,6 @@ export interface ConfigEntry {
supports_remove_device: boolean;
supports_unload: boolean;
supports_reconfigure: boolean;
supported_subentry_types: Record<string, { supports_reconfigure: boolean }>;
num_subentries: number;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;
@@ -29,30 +27,6 @@ export interface ConfigEntry {
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<
Pick<
ConfigEntry,

View File

@@ -2,11 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
import type { HaFormSchema } from "../components/ha-form/types";
import type { ConfigEntry } from "./config_entries";
export type FlowType =
| "config_flow"
| "config_subentries_flow"
| "options_flow"
| "repair_flow";
export type FlowType = "config_flow" | "options_flow" | "repair_flow";
export interface DataEntryFlowProgressedEvent {
type: "data_entry_flow_progressed";

View File

@@ -17,7 +17,6 @@ export {
export interface DeviceRegistryEntry extends RegistryEntry {
id: string;
config_entries: string[];
config_entries_subentries: Record<string, (string | null)[]>;
connections: [string, string][];
identifiers: [string, string][];
manufacturer: string | null;

View File

@@ -50,7 +50,6 @@ export interface EntityRegistryEntry extends RegistryEntry {
icon: string | null;
platform: string;
config_entry_id: string | null;
config_subentry_id: string | null;
device_id: string | null;
area_id: string | null;
labels: string[];

View File

@@ -1,5 +1,6 @@
import { atLeastVersion } from "../../common/config/version";
import type { HomeAssistant } from "../../types";
import { handleFetchPromise } from "../../util/hass-call-api";
import type { HassioResponse } from "./common";
import { hassioApiResultExtractor } from "./common";
@@ -81,24 +82,34 @@ export const fetchHassioBackups = async (
};
export const fetchHassioBackupInfo = async (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
backup: string
): Promise<HassioBackupDetail> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
if (hass) {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
)
);
}
// When called from onboarding we don't have hass
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioBackupDetail>>(
"GET",
`hassio/${
atLeastVersion(hass.config.version, 2021, 9) ? "backups" : "snapshots"
}/${backup}/info`
await handleFetchPromise(
fetch(`/api/hassio/backups/${backup}/info`, {
method: "GET",
})
)
);
};
@@ -229,15 +240,24 @@ export const uploadBackup = async (
};
export const restoreBackup = async (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
type: HassioBackupDetail["type"],
backupSlug: string,
backupDetails: HassioPartialBackupCreateParams | HassioFullBackupCreateParams,
useBackupUrl: boolean
useSnapshotUrl: boolean
): Promise<void> => {
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useBackupUrl ? "backups" : "snapshots"}/${backupSlug}/restore/${type}`,
backupDetails
);
if (hass) {
await hass.callApi<HassioResponse<{ job_id: string }>>(
"POST",
`hassio/${useSnapshotUrl ? "snapshots" : "backups"}/${backupSlug}/restore/${type}`,
backupDetails
);
} else {
await handleFetchPromise(
fetch(`/api/hassio/backups/${backupSlug}/restore/${type}`, {
method: "POST",
body: JSON.stringify(backupDetails),
})
);
}
};

View File

@@ -1,46 +0,0 @@
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}`);

View File

@@ -63,7 +63,6 @@ export type TranslationCategory =
| "entity_component"
| "exceptions"
| "config"
| "config_subentries"
| "config_panel"
| "options"
| "device_automation"

View File

@@ -282,8 +282,6 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.navigateToResult=${this._params
.navigateToResult}
></step-flow-create-entry>
`}
`}

View File

@@ -77,7 +77,7 @@ export class FlowPreviewGeneric extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
if (this.flowType === "repair_flow") {
return;
}
try {

View File

@@ -147,7 +147,7 @@ class FlowPreviewTemplate extends LitElement {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
if (this.flowType === "repair_flow") {
return;
}
try {

View File

@@ -16,9 +16,7 @@ export const loadConfigFlowDialog = loadDataEntryFlowDialog;
export const showConfigFlowDialog = (
element: HTMLElement,
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig"> & {
entryId?: string;
}
dialogParams: Omit<DataEntryFlowDialogParams, "flowConfig">
): void =>
showFlowDialog(element, dialogParams, {
flowType: "config_flow",

View File

@@ -148,8 +148,8 @@ export interface DataEntryFlowDialogParams {
}) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
entryId?: string;
dialogParentElement?: HTMLElement;
navigateToResult?: boolean;
}
export const loadDataEntryFlowDialog = () => import("./dialog-data-entry-flow");

View File

@@ -1,275 +0,0 @@
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"
),
}
);
},
});

View File

@@ -60,7 +60,6 @@ class StepFlowAbort extends LitElement {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});
},
});

View File

@@ -29,8 +29,6 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry;
navigateToResult = false;
private _devices = memoizeOne(
(
showDevices: boolean,
@@ -85,7 +83,6 @@ class StepFlowCreateEntry extends LitElement {
assistSatelliteSupportsSetupFlow(this.hass.states[satellite.entity_id])
)
) {
this.navigateToResult = false;
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: devices[0].id,
@@ -156,7 +153,7 @@ class StepFlowCreateEntry extends LitElement {
private _flowDone(): void {
fireEvent(this, "flow-update", { step: undefined });
if (this.step.result && this.navigateToResult) {
if (this.step.result) {
navigate(
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
);

View File

@@ -49,8 +49,6 @@ class LightRgbColorPicker extends LitElement {
@state() private _hsPickerValue?: [number, number];
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -213,10 +211,7 @@ class LightRgbColorPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (
this._isInteracting ||
(!changedProps.has("entityId") && !changedProps.has("hass"))
) {
if (!changedProps.has("entityId") && !changedProps.has("hass")) {
return;
}
@@ -224,13 +219,10 @@ class LightRgbColorPicker extends LitElement {
}
private _hsColorCursorMoved(ev: CustomEvent) {
const color = ev.detail.value;
this._isInteracting = color !== undefined;
if (color === undefined) {
if (!ev.detail.value) {
return;
}
this._hsPickerValue = color;
this._hsPickerValue = ev.detail.value;
this._throttleUpdateColor();
}

View File

@@ -22,6 +22,7 @@ import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity_attributes";
declare global {
interface HASSDomEvents {
"color-changed": LightColor;
"color-hovered": LightColor | undefined;
}
}
@@ -53,8 +54,6 @@ class LightColorTempPicker extends LitElement {
@state() private _ctPickerValue?: number;
@state() private _isInteracting?: boolean;
protected render() {
if (!this.stateObj) {
return nothing;
@@ -114,7 +113,7 @@ class LightColorTempPicker extends LitElement {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (this._isInteracting || !changedProps.has("stateObj")) {
if (!changedProps.has("stateObj")) {
return;
}
@@ -124,14 +123,16 @@ class LightColorTempPicker extends LitElement {
private _ctColorCursorMoved(ev: CustomEvent) {
const ct = ev.detail.value;
this._isInteracting = ct !== undefined;
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}
this._ctPickerValue = ct;
fireEvent(this, "color-hovered", {
color_temp_kelvin: ct,
});
this._throttleUpdateColorTemp();
}
@@ -142,6 +143,8 @@ class LightColorTempPicker extends LitElement {
private _ctColorChanged(ev: CustomEvent) {
const ct = ev.detail.value;
fireEvent(this, "color-hovered", undefined);
if (isNaN(ct) || this._ctPickerValue === ct) {
return;
}

View File

@@ -17,7 +17,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
@property({ attribute: false }) public localize: LocalizeFunc = empty;
// Use browser language setup before login.
@property() public language: string = getLocalLanguage();
@property() public language?: string = getLocalLanguage();
@property() public translationFragment?: string;

View File

@@ -41,7 +41,6 @@ import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import "./onboarding-welcome";
import "./onboarding-restore-backup";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { navigate } from "../common/navigate";
@@ -158,9 +157,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
private _renderStep() {
if (this._restoring) {
return html`<onboarding-restore-backup
.hass=${this.hass}
.localize=${this.localize}
.supervisor=${this._supervisor ?? false}
.language=${this.language}
>
</onboarding-restore-backup>`;
}
@@ -168,6 +166,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
if (this._init) {
return html`<onboarding-welcome
.localize=${this.localize}
.language=${this.language}
.supervisor=${this._supervisor}
></onboarding-welcome>`;
}
@@ -236,7 +236,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
if (changedProps.has("language")) {
document.querySelector("html")!.setAttribute("lang", this.language);
document.querySelector("html")!.setAttribute("lang", this.language!);
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@@ -272,6 +272,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
"Home Assistant OS",
"Home Assistant Supervised",
].includes(response.installation_type);
if (this._supervisor) {
// Only load if we have supervisor
import("./onboarding-restore-backup");
}
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(
@@ -450,7 +454,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser),
]);
this.initializeHass(auth, conn);
if (this.language !== this.hass!.language) {
if (this.language && this.language !== this.hass!.language) {
this._updateHass({
locale: { ...this.hass!.locale, language: this.language },
language: this.language,

View File

@@ -1,336 +1,136 @@
import type { TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "./restore-backup/onboarding-restore-backup-upload";
import "./restore-backup/onboarding-restore-backup-details";
import "./restore-backup/onboarding-restore-backup-restore";
import "./restore-backup/onboarding-restore-backup-status";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import "../../hassio/src/components/hassio-upload-backup";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-ansi-to-html";
import "../components/ha-card";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-circular-progress";
import "../components/ha-alert";
import "../components/ha-button";
import { fetchInstallationType } from "../data/onboarding";
import type { HomeAssistant } from "../types";
import "./onboarding-loading";
import { onBoardingStyles } from "./styles";
import { removeSearchParam } from "../common/url/search-params";
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")
class OnboardingRestoreBackup extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public language!: string;
@property({ type: Boolean }) public supervisor = false;
@state() private _restoring = false;
@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;
@state() private _backupSlug?: string;
protected render(): TemplateResult {
return html`
${
this._view !== "status" || this._failed
? html`<ha-icon-button-arrow-prev
.label=${this.localize("ui.panel.page-onboarding.restore.back")}
@click=${this._back}
></ha-icon-button-arrow-prev>`
: nothing
}
</ha-icon-button>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
${
this._error || (this._failed && this._view !== "status")
? html`<ha-alert
alert-type="error"
.title=${this._failed && this._view !== "status"
? this.localize("ui.panel.page-onboarding.restore.failed")
: ""}
>
${this._failed && this._view !== "status"
? this.localize(
`ui.panel.page-onboarding.restore.${this._backupInfo?.last_non_idle_event?.reason === "password_incorrect" ? "failed_wrong_password_description" : "failed_description"}`
)
: 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
${this._restoring
? html`<h1>
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</h1>
<ha-alert alert-type="info">
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</ha-alert>
<onboarding-loading></onboarding-loading>`
: html` <h1>
${this.localize("ui.panel.page-onboarding.restore.header")}
</h1>
<hassio-upload-backup
@backup-uploaded=${this._backupUploaded}
@backup-cleared=${this._backupCleared}
.hass=${this.hass}
.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
}
></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")}
</ha-button>`
: nothing}
</div>
`;
}
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) {
super.firstUpdated(changedProps);
this._loadBackupInfo();
}
private async _loadBackupInfo() {
let onboardingInfo: BackupOnboardingConfig;
try {
onboardingInfo = await fetchBackupOnboardingInfo();
} catch (err: any) {
if (this._restoreRunning) {
private async _checkRestoreStatus(): Promise<void> {
if (this._restoring) {
try {
await fetchInstallationType();
} catch (err: any) {
if (
err.error === "Request error" ||
// core can restart but haven't loaded the backup integration yet
(err.status_code === 500 && err.body?.error === "backup_disabled")
(err as Error).message === "unauthorized" ||
(err as Error).message === "not_found"
) {
// 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("/");
return;
}
}
this._error = err?.message || "Cannot get backup info";
// if we are in an unknown state, show upload
if (this._view === "loading") {
this._view = "upload";
}
return;
}
const {
last_non_idle_event: lastNonIdleEvent,
state: currentState,
backups,
} = onboardingInfo;
this._backupInfo = {
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")}`);
}
}
private _restore(ev: CustomEvent) {
if (!this._backup || !ev.detail.selectedData) {
return;
}
this._selectedData = ev.detail.selectedData;
this._view = "confirm_restore";
private _scheduleCheckRestoreStatus(): void {
setTimeout(() => this._checkRestoreStatus(), 1000);
}
private _reupload() {
this._backup = undefined;
this._backupId = undefined;
this._view = "upload";
private _showBackupDialog(): void {
showHassioBackupDialog(this, {
slug: this._backupSlug!,
onboarding: true,
localize: this.localize,
onRestoring: () => {
this._restoring = true;
this._scheduleCheckRestoreStatus();
},
});
}
static styles = [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
position: relative;
}
ha-icon-button-arrow-prev {
position: absolute;
top: 12px;
}
ha-card {
width: 100%;
}
.loading {
display: flex;
justify-content: center;
padding: 32px;
}
.backup-summary-wrapper {
margin-top: 24px;
padding: 0 20px;
}
`,
];
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
align-items: center;
}
hassio-upload-backup {
width: 100%;
}
.footer {
display: flex;
justify-content: space-between;
width: 100%;
}
`,
];
}
}
declare global {

View File

@@ -1,5 +1,5 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
@@ -13,6 +13,8 @@ class OnboardingWelcome extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ type: Boolean }) public supervisor = false;
protected render(): TemplateResult {
return html`
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
@@ -22,9 +24,11 @@ class OnboardingWelcome extends LitElement {
${this.localize("ui.panel.page-onboarding.welcome.start")}
</ha-button>
<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>
${this.supervisor
? html`<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>`
: nothing}
`;
}

View File

@@ -1,57 +0,0 @@
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;
}
}

View File

@@ -1,175 +0,0 @@
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";
}
}

View File

@@ -1,119 +0,0 @@
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";
}
}

View File

@@ -1,126 +0,0 @@
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;
}
}

View File

@@ -1,4 +1,5 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiAlertCircleCheck,
mdiArrowDown,
@@ -26,9 +27,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -241,104 +240,89 @@ export default class HaAutomationActionRow extends LitElement {
</div> `
: nothing}
<ha-md-button-menu
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable}
>
<ha-list-item graphic="icon" .disabled=${!this._uiModeAvailable}>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
@@ -347,15 +331,15 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
</ha-list-item>
<ha-list-item
class="warning"
.clickAction=${this._onDelete}
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -363,11 +347,11 @@ export default class HaAutomationActionRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="start"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-list-item>
</ha-button-menu>
<div
class=${classMap({
@@ -440,6 +424,47 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._runAction();
break;
case 1:
await this._renameAction();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -447,16 +472,16 @@ export default class HaAutomationActionRow extends LitElement {
};
}
private _onDisable = () => {
private _onDisable() {
const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
};
}
private _runAction = async () => {
private async _runAction() {
const validated = await validateConfig(this.hass, {
actions: this.action,
});
@@ -488,9 +513,9 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.run_action_success"
),
});
};
}
private _onDelete = () => {
private _onDelete() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_title"
@@ -505,7 +530,7 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
};
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
@@ -536,7 +561,7 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlMode = true;
}
private _renameAction = async (): Promise<void> => {
private async _renameAction(): Promise<void> {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.change_alias"
@@ -573,37 +598,7 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlEditor?.setValue(value);
}
}
};
private _duplicateAction = () => {
fireEvent(this, "duplicate");
};
private _copyAction = () => {
this._setClipboard();
};
private _cutAction = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
}
public expand() {
this.updateComplete.then(() => {
@@ -615,6 +610,7 @@ export default class HaAutomationActionRow extends LitElement {
return [
haStyle,
css`
ha-button-menu,
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
@@ -653,11 +649,18 @@ export default class HaAutomationActionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius, 12px);
border-top-left-radius: var(--ha-card-border-radius, 12px);
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
mwc-list-item.hidden {
display: none;
}
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];

View File

@@ -1,4 +1,5 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiArrowDown,
mdiArrowUp,
@@ -23,12 +24,11 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import type {
AutomationClipboard,
Condition,
@@ -141,12 +141,12 @@ export default class HaAutomationConditionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
fixed
>
<ha-icon-button
slot="trigger"
@@ -155,91 +155,76 @@ export default class HaAutomationConditionRow extends LitElement {
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._warnings}
>
<ha-list-item graphic="icon" .disabled=${this._warnings}>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
@@ -248,15 +233,15 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
</ha-list-item>
<ha-list-item
class="warning"
.clickAction=${this._onDelete}
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -264,11 +249,11 @@ export default class HaAutomationConditionRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="start"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-list-item>
</ha-button-menu>
<div
class=${classMap({
@@ -340,6 +325,47 @@ export default class HaAutomationConditionRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._testCondition();
break;
case 1:
await this._renameCondition();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -347,13 +373,13 @@ export default class HaAutomationConditionRow extends LitElement {
};
}
private _onDisable = () => {
private _onDisable() {
const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value });
};
}
private _onDelete = () => {
private _onDelete() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete_confirm_title"
@@ -368,7 +394,7 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
};
}
private _switchUiMode() {
this._warnings = undefined;
@@ -380,7 +406,7 @@ export default class HaAutomationConditionRow extends LitElement {
this._yamlMode = true;
}
private _testCondition = async () => {
private async _testCondition() {
if (this._testing) {
return;
}
@@ -435,9 +461,9 @@ export default class HaAutomationConditionRow extends LitElement {
this._testing = false;
}, 2500);
}
};
}
private _renameCondition = async (): Promise<void> => {
private async _renameCondition(): Promise<void> {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.change_alias"
@@ -463,37 +489,7 @@ export default class HaAutomationConditionRow extends LitElement {
value,
});
}
};
private _duplicateCondition = () => {
fireEvent(this, "duplicate");
};
private _copyCondition = () => {
this._setClipboard();
};
private _cutCondition = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
}
public expand() {
this.updateComplete.then(() => {
@@ -505,6 +501,9 @@ export default class HaAutomationConditionRow extends LitElement {
return [
haStyle,
css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
@@ -540,6 +539,12 @@ export default class HaAutomationConditionRow extends LitElement {
border-top-right-radius: var(--ha-card-border-radius, 12px);
border-top-left-radius: var(--ha-card-border-radius, 12px);
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
.testing {
position: absolute;
top: 0px;
@@ -566,8 +571,8 @@ export default class HaAutomationConditionRow extends LitElement {
.testing.pass {
background-color: var(--success-color);
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];

View File

@@ -339,8 +339,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
sortable: true,
groupable: true,
hidden: narrow,
title: "",
type: "overflow",
title: this.hass.localize("ui.panel.config.automation.picker.state"),
label: this.hass.localize("ui.panel.config.automation.picker.state"),
template: (automation) => html`
<ha-entity-toggle
.stateObj=${automation}

View File

@@ -1,4 +1,5 @@
import { consume } from "@lit-labs/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiArrowDown,
mdiArrowUp,
@@ -27,9 +28,7 @@ import { capitalizeFirstLetter } from "../../../../common/string/capitalize-firs
import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -170,12 +169,12 @@ export default class HaAutomationTriggerRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
fixed
>
<ha-icon-button
slot="trigger"
@@ -183,93 +182,84 @@ export default class HaAutomationTriggerRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this._renameTrigger}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
</ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon
></ha-list-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported}
>
<ha-list-item graphic="icon" .disabled=${!supported}>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<li divider role="separator"></li>
<ha-md-menu-item
.clickAction=${this._onDisable}
<ha-list-item
graphic="icon"
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
@@ -280,16 +270,16 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
slot="graphic"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
</ha-list-item>
<ha-list-item
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -297,11 +287,11 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
<ha-svg-icon
class="warning"
slot="start"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-list-item>
</ha-button-menu>
<div
class=${classMap({
@@ -474,6 +464,48 @@ export default class HaAutomationTriggerRow extends LitElement {
}
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._renameTrigger();
break;
case 1:
this._requestShowId = true;
this.expand();
break;
case 2:
fireEvent(this, "duplicate");
break;
case 3:
this._setClipboard();
break;
case 4:
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
break;
case 5:
fireEvent(this, "move-up");
break;
case 6:
fireEvent(this, "move-down");
break;
case 7:
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
break;
case 8:
this._onDisable();
break;
case 9:
this._onDelete();
break;
}
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -481,7 +513,7 @@ export default class HaAutomationTriggerRow extends LitElement {
};
}
private _onDelete = () => {
private _onDelete() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm_title"
@@ -496,9 +528,9 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "value-changed", { value: null });
},
});
};
}
private _onDisable = () => {
private _onDisable() {
if (isTriggerList(this.trigger)) return;
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
@@ -506,7 +538,7 @@ export default class HaAutomationTriggerRow extends LitElement {
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
};
}
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
@@ -573,7 +605,7 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _renameTrigger = async (): Promise<void> => {
private async _renameTrigger(): Promise<void> {
if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
title: this.hass.localize(
@@ -604,42 +636,7 @@ export default class HaAutomationTriggerRow extends LitElement {
this._yamlEditor?.setValue(value);
}
}
};
private _showTriggerId = () => {
this._requestShowId = true;
this.expand();
};
private _duplicateTrigger = () => {
fireEvent(this, "duplicate");
};
private _copyTrigger = () => {
this._setClipboard();
};
private _cutTrigger = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
};
private _moveUp = () => {
fireEvent(this, "move-up");
};
private _moveDown = () => {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
if (this._yamlMode) {
this._switchUiMode();
} else {
this._switchYamlMode();
}
this.expand();
};
}
public expand() {
this.updateComplete.then(() => {
@@ -651,6 +648,9 @@ export default class HaAutomationTriggerRow extends LitElement {
return [
haStyle,
css`
ha-button-menu {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
@@ -714,12 +714,18 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];

View File

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
@@ -23,6 +22,7 @@ import {
import type { CloudStatus } from "../../../../../data/cloud";
import type { HomeAssistant } from "../../../../../types";
import { brandsUrl } from "../../../../../util/brands-url";
import { navigate } from "../../../../../common/navigate";
const DEFAULT_AGENTS = [];

View File

@@ -46,7 +46,7 @@ enum BackupScheduleTime {
}
interface RetentionData {
type: "copies" | "days" | "forever";
type: "copies" | "days";
value: number;
}
@@ -55,7 +55,7 @@ const RETENTION_PRESETS: Record<
RetentionData
> = {
copies_3: { type: "copies", value: 3 },
forever: { type: "forever", value: 0 },
forever: { type: "days", value: 0 },
};
const SCHEDULE_OPTIONS = [
@@ -79,10 +79,7 @@ const computeRetentionPreset = (
data: RetentionData
): RetentionPreset | undefined => {
for (const [key, value] of Object.entries(RETENTION_PRESETS)) {
if (
value.type === data.type &&
(value.type === RetentionPreset.FOREVER || value.value === data.value)
) {
if (value.type === data.type && value.value === data.value) {
return key as RetentionPreset;
}
}
@@ -95,7 +92,7 @@ interface FormData {
time?: string | null;
days: BackupDay[];
retention: {
type: "copies" | "days" | "forever";
type: "copies" | "days";
value: number;
};
}
@@ -145,12 +142,7 @@ class HaBackupConfigSchedule extends LitElement {
? config.schedule.days
: [],
retention: {
type:
config.retention.days === null && config.retention.copies === null
? "forever"
: config.retention.days != null
? "days"
: "copies",
type: config.retention.days != null ? "days" : "copies",
value: config.retention.days ?? config.retention.copies ?? 3,
},
};
@@ -168,11 +160,9 @@ class HaBackupConfigSchedule extends LitElement {
: [],
},
retention:
data.retention.type === "forever"
? { days: null, copies: null }
: data.retention.type === "days"
? { days: data.retention.value, copies: null }
: { copies: data.retention.value, days: null },
data.retention.type === "days"
? { days: data.retention.value, copies: null }
: { copies: data.retention.value, days: null },
};
fireEvent(this, "value-changed", { value: this.value });
@@ -491,19 +481,9 @@ class HaBackupConfigSchedule extends LitElement {
private _retentionPresetChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdSelect;
let value = target.value as RetentionPreset;
// custom needs to have a type of days or copies, set it to default copies 3
if (
value === RetentionPreset.CUSTOM &&
this._retentionPreset === RetentionPreset.FOREVER
) {
this._retentionPreset = value;
value = RetentionPreset.COPIES_3;
} else {
this._retentionPreset = value;
}
const value = target.value as RetentionPreset;
this._retentionPreset = value;
if (value !== RetentionPreset.CUSTOM) {
const data = this._getData(this.value);
const retention = RETENTION_PRESETS[value];
@@ -513,7 +493,7 @@ class HaBackupConfigSchedule extends LitElement {
}
this._setData({
...data,
retention,
retention: RETENTION_PRESETS[value],
});
}
}
@@ -524,7 +504,6 @@ class HaBackupConfigSchedule extends LitElement {
const value = parseInt(target.value);
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
const data = this._getData(this.value);
target.value = clamped.toString();
this._setData({
...data,
retention: {

View File

@@ -21,7 +21,7 @@ export interface BackupAddonItem {
@customElement("ha-backup-addons-picker")
export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addons!: BackupAddonItem[];
@@ -32,7 +32,7 @@ export class HaBackupAddonsPicker extends LitElement {
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
addons.sort((a, b) =>
stringCompare(a.name, b.name, this.hass?.locale?.language)
stringCompare(a.name, b.name, this.hass.locale.language)
)
);

View File

@@ -47,32 +47,23 @@ interface SelectedItems {
@customElement("ha-backup-data-picker")
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 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> = {};
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this.hass && isComponentLoaded(this.hass, "hassio")) {
if (isComponentLoaded(this.hass, "hassio")) {
this._fetchAddonInfo();
}
}
private async _fetchAddonInfo() {
const { addons } = await fetchHassioAddonsInfo(this.hass!);
const { addons } = await fetchHassioAddonsInfo(this.hass);
this._addonIcons = addons.reduce<Record<string, boolean>>(
(acc, addon) => ({
...acc,
@@ -83,14 +74,16 @@ export class HaBackupDataPicker extends LitElement {
}
private _homeAssistantItems = memoizeOne(
(data: BackupData, localize: LocalizeFunc) => {
(data: BackupData, _localize: LocalizeFunc) => {
const items: CheckBoxItem[] = [];
if (data.homeassistant_included) {
items.push({
label: localize(
`ui.panel.${this.translationKeyPanel}.data_picker.${data.database_included ? "settings_and_history" : "settings"}`
),
label: data.database_included
? this.hass.localize(
"ui.panel.config.backup.data_picker.settings_and_history"
)
: this.hass.localize("ui.panel.config.backup.data_picker.settings"),
id: "config",
version: data.homeassistant_version,
});
@@ -106,22 +99,18 @@ export class HaBackupDataPicker extends LitElement {
);
private _localizeFolder(folder: string): string {
const localize = this.localize || this.hass!.localize;
switch (folder) {
case "media":
return localize(
`ui.panel.${this.translationKeyPanel}.data_picker.media`
);
return this.hass.localize("ui.panel.config.backup.data_picker.media");
case "share":
return localize(
`ui.panel.${this.translationKeyPanel}.data_picker.share_folder`
return this.hass.localize(
"ui.panel.config.backup.data_picker.share_folder"
);
case "ssl":
return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`);
return this.hass.localize("ui.panel.config.backup.data_picker.ssl");
case "addons/local":
return localize(
`ui.panel.${this.translationKeyPanel}.data_picker.local_addons`
return this.hass.localize(
"ui.panel.config.backup.data_picker.local_addons"
);
}
return capitalizeFirstLetter(folder);
@@ -226,13 +215,14 @@ export class HaBackupDataPicker extends LitElement {
}
protected render() {
const localize = this.localize || this.hass!.localize;
const homeAssistantItems = this._homeAssistantItems(this.data, localize);
const homeAssistantItems = this._homeAssistantItems(
this.data,
this.hass.localize
);
const addonsItems = this._addonsItems(
this.data,
localize,
this.hass.localize,
this._addonIcons
);
@@ -257,7 +247,6 @@ export class HaBackupDataPicker extends LitElement {
selectedItems.homeassistant.length <
homeAssistantItems.length}
@change=${this._sectionChanged}
?disabled=${this.requiredItems.length > 0}
></ha-checkbox>
</ha-formfield>
<div class="items">
@@ -277,7 +266,6 @@ export class HaBackupDataPicker extends LitElement {
item.id
)}
@change=${this._homeassistantChanged}
.disabled=${this.requiredItems.includes(item.id)}
></ha-checkbox>
</ha-formfield>
`
@@ -292,8 +280,8 @@ export class HaBackupDataPicker extends LitElement {
<ha-formfield>
<ha-backup-formfield-label
slot="label"
.label=${localize(
`ui.panel.${this.translationKeyPanel}.data_picker.addons`
.label=${this.hass.localize(
"ui.panel.config.backup.data_picker.addons"
)}
.iconPath=${mdiPuzzle}
>

View File

@@ -1,148 +0,0 @@
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 };
}
}

View File

@@ -1,153 +0,0 @@
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;
}
}

View File

@@ -3,10 +3,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
fireEvent,
type HASSDomEvent,
} from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-expansion-panel";
@@ -17,10 +14,7 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
CORE_LOCAL_AGENT,
HASSIO_LOCAL_AGENT,
SUPPORTED_UPLOAD_FORMAT,
uploadBackup,
INITIAL_UPLOAD_FORM_DATA,
type BackupUploadFileFormData,
} from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@@ -28,6 +22,16 @@ import type { HomeAssistant } from "../../../../types";
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
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")
export class DialogUploadBackup
extends LitElement
@@ -41,13 +45,13 @@ export class DialogUploadBackup
@state() private _error?: string;
@state() private _formData?: BackupUploadFileFormData;
@state() private _formData?: FormData;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: UploadBackupDialogParams): Promise<void> {
this._params = params;
this._formData = INITIAL_UPLOAD_FORM_DATA;
this._formData = INITIAL_DATA;
}
private _dialogClosed() {
@@ -74,18 +78,13 @@ export class DialogUploadBackup
}
return html`
<ha-md-dialog
open
@closed=${this._dialogClosed}
.disableCancelAction=${this._uploading}
>
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._uploading}
></ha-icon-button>
<span slot="title">
@@ -100,8 +99,7 @@ export class DialogUploadBackup
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
.accept=${SUPPORTED_UPLOAD_FORMAT}
.localize=${this.hass.localize}
accept=${SUPPORTED_FORMAT}
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.input_label"
)}
@@ -109,17 +107,13 @@ export class DialogUploadBackup
"ui.panel.config.backup.dialogs.upload.supports_tar"
)}
@file-picked=${this._filePicked}
@files-cleared=${this._filesCleared}
></ha-file-upload>
</div>
<div slot="actions">
<ha-button @click=${this.closeDialog} .disabled=${this._uploading}
<ha-button @click=${this.closeDialog}
>${this.hass.localize("ui.common.cancel")}</ha-button
>
<ha-button
@click=${this._upload}
.disabled=${!this._formValid() || this._uploading}
>
<ha-button @click=${this._upload} .disabled=${!this._formValid()}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.upload.action"
)}
@@ -129,7 +123,7 @@ export class DialogUploadBackup
`;
}
private _filePicked(ev: HASSDomEvent<{ files: File[] }>) {
private async _filePicked(ev: CustomEvent<{ files: File[] }>): Promise<void> {
this._error = undefined;
const file = ev.detail.files[0];
@@ -139,14 +133,9 @@ export class DialogUploadBackup
};
}
private _filesCleared() {
this._error = undefined;
this._formData = INITIAL_UPLOAD_FORM_DATA;
}
private async _upload() {
const { file } = this._formData!;
if (!file || file.type !== SUPPORTED_UPLOAD_FORMAT) {
if (!file || file.type !== SUPPORTED_FORMAT) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.backup.dialogs.upload.unsupported.title"
@@ -165,7 +154,7 @@ export class DialogUploadBackup
this._uploading = true;
try {
await uploadBackup(this.hass, file, agentIds);
await uploadBackup(this.hass!, file, agentIds);
this._params!.submit?.();
this.closeDialog();
} catch (err: any) {

View File

@@ -8,7 +8,7 @@ import {
mdiUpload,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@@ -27,7 +27,6 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-circular-progress";
import "../../../components/ha-fab";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
@@ -461,17 +460,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon">
<ha-circular-progress
.size=${"small"}
indeterminate
></ha-circular-progress>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: nothing}
@@ -616,14 +605,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-circular-progress {
--md-sys-color-primary: var(--mdc-theme-on-secondary);
}
`,
];
return haStyle;
}
}

View File

@@ -8,6 +8,7 @@ import {
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { computeDomain } from "../../../common/entity/compute_domain";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-alert";
@@ -24,20 +25,24 @@ import type {
BackupConfig,
BackupContentAgent,
BackupContentExtended,
BackupData,
} from "../../../data/backup";
import "./components/ha-backup-details-summary";
import "./components/ha-backup-details-restore";
import {
compareAgents,
computeBackupAgentName,
computeBackupSize,
computeBackupType,
deleteBackup,
fetchBackupDetails,
isLocalAgent,
isNetworkMountAgent,
} from "../../../data/backup";
import type { HassioAddonInfo } from "../../../data/hassio/addon";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
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 { fireEvent } from "../../../common/dom/fire_event";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
@@ -88,6 +93,10 @@ class HaConfigBackupDetails extends LitElement {
@state() private _error?: string;
@state() private _selectedData?: BackupData;
@state() private _addonsInfo?: HassioAddonInfo[];
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
@@ -148,18 +157,81 @@ class HaConfigBackupDetails extends LitElement {
: !this._backup
? html`<ha-circular-progress active></ha-circular-progress>`
: html`
<ha-backup-details-summary
.backup=${this._backup}
.hass=${this.hass}
.localize=${this.hass.localize}
.isHassio=${isHassio}
></ha-backup-details-summary>
<ha-backup-details-restore
.backup=${this._backup}
@backup-restore=${this._restore}
.hass=${this.hass}
.localize=${this.hass.localize}
></ha-backup-details-restore>
<ha-card>
<div class="card-header">
${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.backup_type"
)}
</span>
<span slot="supporting-text">
${this.hass.localize(
`ui.panel.config.backup.type.${computeBackupType(this._backup, isHassio)}`
)}
</span>
</ha-md-list-item>
<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}
.data=${this._backup}
.value=${this._selectedData}
@value-changed=${this._selectedBackupChanged}
.addonsInfo=${this._addonsInfo}
>
</ha-backup-data-picker>
</div>
<div class="card-actions">
<ha-button
@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>
<div class="card-header">
${this.hass.localize(
@@ -288,13 +360,30 @@ class HaConfigBackupDetails extends LitElement {
`;
}
private _restore(ev: CustomEvent) {
if (!this._backup || !ev.detail.selectedData) {
private _selectedBackupChanged(ev: CustomEvent) {
ev.stopPropagation();
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;
}
showRestoreBackupDialog(this, {
backup: this._backup,
selectedData: ev.detail.selectedData,
selectedData: this._selectedData,
});
}
@@ -380,6 +469,13 @@ class HaConfigBackupDetails extends LitElement {
--mdc-icon-size: 48px;
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 {
color: var(--error-color);
}
@@ -389,6 +485,9 @@ class HaConfigBackupDetails extends LitElement {
ha-button.danger {
--mdc-theme-primary: var(--error-color);
}
ha-backup-data-picker {
display: block;
}
ha-md-list-item [slot="supporting-text"] {
display: flex;
align-items: center;

View File

@@ -8,7 +8,6 @@ import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-fab";
import "../../../components/ha-circular-progress";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
@@ -18,10 +17,8 @@ import type {
BackupAgent,
BackupConfig,
BackupContent,
BackupInfo,
} from "../../../data/backup";
import {
computeBackupAgentName,
generateBackup,
generateBackupWithAutomaticSettings,
} from "../../../data/backup";
@@ -53,8 +50,6 @@ class HaConfigBackupOverview extends LitElement {
@property({ attribute: false }) public manager!: ManagerStateEvent;
@property({ attribute: false }) public info?: BackupInfo;
@property({ attribute: false }) public backups: BackupContent[] = [];
@property({ attribute: false }) public fetching = false;
@@ -156,26 +151,6 @@ class HaConfigBackupOverview extends LitElement {
</ha-list-item>
</ha-button-menu>
<div class="content">
${this.info && Object.keys(this.info.agent_errors).length
? html`${Object.entries(this.info.agent_errors).map(
([agentId, error]) =>
html`<ha-alert
alert-type="error"
.title=${this.hass.localize(
"ui.panel.config.backup.overview.agent_error",
{
name: computeBackupAgentName(
this.hass.localize,
agentId,
this.agents
),
}
)}
>
${error}
</ha-alert>`
)}`
: nothing}
${backupInProgress
? html`
<ha-backup-overview-progress
@@ -229,14 +204,7 @@ class HaConfigBackupOverview extends LitElement {
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon">
<ha-circular-progress
.size=${"small"}
indeterminate
></ha-circular-progress>
</div>`
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-subpage>
`;
@@ -263,9 +231,6 @@ class HaConfigBackupOverview extends LitElement {
padding-left: 0;
padding-right: 0;
}
ha-circular-progress {
--md-sys-color-primary: var(--mdc-theme-on-secondary);
}
`,
];
}

View File

@@ -1,4 +1,4 @@
import { mdiDotsVertical, mdiHarddisk, mdiOpenInNew } from "@mdi/js";
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -28,7 +28,6 @@ import "./components/config/ha-backup-config-encryption-key";
import "./components/config/ha-backup-config-schedule";
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("ha-config-backup-settings")
class HaConfigBackupSettings extends LitElement {
@@ -99,8 +98,6 @@ class HaConfigBackupSettings extends LitElement {
return nothing;
}
const supervisor = isComponentLoaded(this.hass, "hassio");
return html`
<hass-subpage
back-path="/config/backup"
@@ -108,7 +105,7 @@ class HaConfigBackupSettings extends LitElement {
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.backup.settings.header")}
>
${supervisor
${isComponentLoaded(this.hass, "hassio")
? html`
<ha-button-menu slot="toolbar-icon">
<ha-icon-button
@@ -206,29 +203,6 @@ class HaConfigBackupSettings extends LitElement {
`
: nothing}
</div>
<div class="card-actions">
<a
href=${documentationUrl(this.hass, "/integrations/#backup")}
target="_blank"
rel="noreferrer"
>
<ha-button>
<ha-svg-icon slot="icon" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.backup.settings.locations.more_locations"
)}
</ha-button>
</a>
${supervisor
? html`<a href="/config/storage">
<ha-button>
${this.hass.localize(
"ui.panel.config.backup.settings.locations.manage_network_storage"
)}
</ha-button>
</a>`
: nothing}
</div>
</ha-card>
<ha-card>
<div class="card-header">
@@ -368,9 +342,6 @@ class HaConfigBackupSettings extends LitElement {
.card-content {
padding-bottom: 0;
}
a {
text-decoration: none;
}
`;
}

View File

@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import type {
BackupAgent,
BackupConfig,
BackupInfo,
BackupContent,
} from "../../../data/backup";
import {
compareAgents,
@@ -44,7 +44,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
@state() private _manager: ManagerStateEvent = DEFAULT_MANAGER_STATE;
@state() private _info?: BackupInfo;
@state() private _backups: BackupContent[] = [];
@state() private _agents: BackupAgent[] = [];
@@ -87,7 +87,8 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
}
private async _fetchBackupInfo() {
this._info = await fetchBackupInfo(this.hass);
const info = await fetchBackupInfo(this.hass);
this._backups = info.backups;
}
private async _fetchBackupConfig() {
@@ -133,8 +134,7 @@ class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
pageEl.narrow = this.narrow;
pageEl.cloudStatus = this.cloudStatus;
pageEl.manager = this._manager;
pageEl.info = this._info;
pageEl.backups = this._info?.backups || [];
pageEl.backups = this._backups;
pageEl.config = this._config;
pageEl.agents = this._agents;
pageEl.fetching = this._fetching;

View File

@@ -51,8 +51,8 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
import type { ConfigEntry } from "../../../data/config_entries";
import { sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import type { DataTableFilters } from "../../../data/data_table_filters";
import {
@@ -108,8 +108,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public entries!: ConfigEntry[];
@state() private _subEntries?: SubEntry[];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
entities!: EntityRegistryEntry[];
@@ -221,7 +219,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
private _setFiltersFromUrl() {
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
const label = this._searchParms.has("label");
if (!domain && !configEntry && !label) {
@@ -246,10 +243,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
value: configEntry ? [configEntry] : [],
items: undefined,
},
sub_entry: {
value: subEntry ? [subEntry] : [],
items: undefined,
},
};
this._filterLabel();
}
@@ -341,32 +334,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
if (configEntries.length === 1) {
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 (
key === "ha-filter-integrations" &&
Array.isArray(filter.value) &&
@@ -788,15 +755,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${this.entries?.find(
(entry) =>
entry.entry_id === 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}
)?.title || this._filters.config_entry.value[0]}
</ha-alert>`
: nothing}
<ha-filter-floor-areas
@@ -929,10 +888,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
`;
}
private async _loadSubEntries(entryId: string) {
this._subEntries = await getSubEntries(this.hass, entryId);
}
private _filterExpanded(ev) {
if (ev.detail.expanded) {
this._expandedFilter = ev.target.localName;

View File

@@ -66,8 +66,8 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getConfigEntries, getSubEntries } from "../../../data/config_entries";
import type { ConfigEntry } from "../../../data/config_entries";
import { getConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import type {
DataTableFiltersItems,
@@ -146,8 +146,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@state() private _entries?: ConfigEntry[];
@state() private _subEntries?: SubEntry[];
@state() private _manifests?: IntegrationManifest[];
@state()
@@ -355,8 +353,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showNarrow: true,
sortable: true,
filterable: true,
minWidth: "80px",
maxWidth: "80px",
template: (entry) =>
entry.unavailable ||
entry.disabled_by ||
@@ -526,27 +522,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
if (configEntries.length === 1) {
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 (
key === "ha-filter-integrations" &&
Array.isArray(filter) &&
@@ -929,22 +904,14 @@ ${
</ha-md-button-menu>
${
Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
this._filters.config_entry?.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
)}
${this._entries?.find(
(entry) => entry.entry_id === 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}
)?.title || this._filters.config_entry[0]}
</ha-alert>`
: nothing
}
@@ -1057,7 +1024,6 @@ ${
private _setFiltersFromUrl() {
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
const label = this._searchParms.has("label");
if (!domain && !configEntry && !label) {
@@ -1070,7 +1036,6 @@ ${
"ha-filter-states": [],
"ha-filter-integrations": domain ? [domain] : [],
config_entry: configEntry ? [configEntry] : [],
sub_entry: subEntry ? [subEntry] : [],
};
this._filterLabel();
}
@@ -1128,7 +1093,6 @@ ${
hidden_by: null,
area_id: null,
config_entry_id: null,
config_subentry_id: null,
device_id: null,
icon: null,
readonly: true,
@@ -1420,10 +1384,6 @@ ${rejected
this._entries = await getConfigEntries(this.hass);
}
private async _loadSubEntries(entryId: string) {
this._subEntries = await getSubEntries(this.hass, entryId);
}
private _addDevice() {
const { filteredConfigEntry, filteredDomains } =
this._filteredEntitiesAndDomains(

View File

@@ -346,11 +346,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
groupable: true,
},
editable: {
title: localize("ui.panel.config.helpers.picker.headers.editable"),
title: "",
label: localize("ui.panel.config.helpers.picker.headers.editable"),
type: "icon",
sortable: true,
minWidth: "88px",
maxWidth: "88px",
showNarrow: true,
template: (helper) => html`
${!helper.editable

View File

@@ -654,7 +654,6 @@ class AddIntegrationDialog extends LitElement {
startFlowHandler: domain,
showAdvanced: this.hass.userData?.showAdvanced,
manifest,
navigateToResult: true,
});
}

View File

@@ -70,7 +70,7 @@ export class HaConfigFlowCard extends LitElement {
? html`<a
href=${this.flow.context.configuration_url.replace(
/^homeassistant:\/\//,
"/"
""
)}
rel="noreferrer"
target=${this.flow.context.configuration_url.startsWith(
@@ -137,7 +137,6 @@ export class HaConfigFlowCard extends LitElement {
}
showConfigFlowDialog(this, {
continueFlowId: this.flow.flow_id,
navigateToResult: true,
dialogClosedCallback: () => {
this._handleFlowUpdated();
},

View File

@@ -16,7 +16,6 @@ import {
mdiOpenInNew,
mdiPackageVariant,
mdiPlayCircleOutline,
mdiPlus,
mdiProgressHelper,
mdiReload,
mdiReloadAlert,
@@ -53,17 +52,14 @@ import { getSignedPath } from "../../../data/auth";
import type {
ConfigEntry,
DisableConfigEntryResult,
SubEntry,
} from "../../../data/config_entries";
import {
ERROR_STATES,
RECOVERABLE_STATES,
deleteConfigEntry,
deleteSubEntry,
disableConfigEntry,
enableConfigEntry,
getConfigEntries,
getSubEntries,
reloadConfigEntry,
updateConfigEntry,
} from "../../../data/config_entries";
@@ -110,7 +106,6 @@ import { fileDownload } from "../../../util/file_download";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import { QUALITY_SCALE_MAP } from "../../../data/integration_quality_scale";
import { showSubConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-sub-config-flow";
export const renderConfigEntryError = (
hass: HomeAssistant,
@@ -177,8 +172,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@state() private _domainEntities: Record<string, string[]> = {};
@state() private _subEntries: Record<string, SubEntry[]> = {};
private _configPanel = memoizeOne(
(domain: string, panels: HomeAssistant["panels"]): string | undefined =>
Object.values(panels).find(
@@ -221,18 +214,11 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("domain")) {
this.hass.loadBackendTranslation("title", [this.domain]);
this.hass.loadBackendTranslation("config_subentries", [this.domain]);
this._extraConfigEntries = undefined;
this._fetchManifest();
this._fetchDiagnostics();
this._fetchEntitySources();
}
if (
changedProperties.has("configEntries") ||
changedProperties.has("_extraConfigEntries")
) {
this._fetchSubEntries();
}
}
private async _fetchEntitySources() {
@@ -587,7 +573,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
const attention = ATTENTION_SOURCES.includes(
flow.context.source
);
return html`<ha-md-list-item
return html` <ha-md-list-item
class="config_entry ${attention ? "attention" : ""}"
>
${flow.localized_title}
@@ -687,73 +673,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
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) {
let stateText: Parameters<typeof this.hass.localize> | undefined;
let stateTextExtra: TemplateResult | string | undefined;
@@ -801,299 +720,274 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
)}.`);
}
} else {
devicesLine = this._renderDeviceLine(item, devices, services, entities);
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}`;
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 subEntries = this._subEntries[item.entry_id] || [];
return html`<ha-md-list-item
class=${classMap({
config_entry: true,
"state-not-loaded": item!.state === "not_loaded",
"state-failed-unload": item!.state === "failed_unload",
"state-setup": item!.state === "setup_in_progress",
"state-error": ERROR_STATES.includes(item!.state),
"state-disabled": item.disabled_by !== null,
})}
data-entry-id=${item.entry_id}
.configEntry=${item}
>
<div slot="headline">
${item.title || domainToName(this.hass.localize, item.domain)}
</div>
<div slot="supporting-text">
<div>${devicesLine}</div>
${stateText
? html`
<div class="message">
<ha-svg-icon .path=${icon}></ha-svg-icon>
<div>
${this.hass.localize(...stateText)}${stateTextExtra
? html`: ${stateTextExtra}`
: nothing}
</div>
class=${classMap({
config_entry: true,
"state-not-loaded": item!.state === "not_loaded",
"state-failed-unload": item!.state === "failed_unload",
"state-setup": item!.state === "setup_in_progress",
"state-error": ERROR_STATES.includes(item!.state),
"state-disabled": item.disabled_by !== null,
})}
data-entry-id=${item.entry_id}
.configEntry=${item}
>
<div slot="headline">
${item.title || domainToName(this.hass.localize, item.domain)}
</div>
<div slot="supporting-text">
<div>${devicesLine}</div>
${stateText
? html`
<div class="message">
<ha-svg-icon .path=${icon}></ha-svg-icon>
<div>
${this.hass.localize(...stateText)}${stateTextExtra
? html`: ${stateTextExtra}`
: nothing}
</div>
`
: nothing}
</div>
${item.disabled_by === "user"
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</ha-button>`
: configPanel &&
(item.domain !== "matter" ||
isDevVersion(this.hass.config.version)) &&
!stateText
? html`<a
slot="end"
href=${`/${configPanel}?config_entry=${item.entry_id}`}
><ha-button>
</div>
`
: nothing}
</div>
${item.disabled_by === "user"
? html`<ha-button unelevated slot="end" @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</ha-button>`
: configPanel &&
(item.domain !== "matter" ||
isDevVersion(this.hass.config.version)) &&
!stateText
? html`<a
slot="end"
href=${`/${configPanel}?config_entry=${item.entry_id}`}
><ha-button>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</ha-button></a
>`
: item.supports_options
? html`
<ha-button slot="end" @click=${this._showOptions}>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.configure"
)}
</ha-button></a
>`
: item.supports_options
? html`
<ha-button slot="end" @click=${this._showOptions}>
${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>
${item.disabled_by && devices.length
? html`
<ha-md-menu-item
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item>
</ha-button>
`
: nothing}
${item.disabled_by && services.length
? html`<ha-md-menu-item
href=${services.length === 1
? `/config/devices/device/${services[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item> `
: nothing}
${item.disabled_by && entities.length
? html`
<ha-md-menu-item
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
.path=${mdiShapeOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item>
`
: nothing}
${!item.disabled_by &&
RECOVERABLE_STATES.includes(item.state) &&
item.supports_unload &&
item.source !== "system"
? html`
<ha-md-menu-item @click=${this._handleReload}>
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
</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>
${this._diagnosticHandler && item.state === "loaded"
? html`
<ha-md-menu-item
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-md-menu-item>
`
: nothing}
${!item.disabled_by &&
item.supports_reconfigure &&
item.source !== "system"
? html`
<ha-md-menu-item @click=${this._handleReconfigure}>
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reconfigure"
)}
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</ha-md-menu-item>
${item.disabled_by === "user"
? html`
<ha-md-menu-item @click=${this._handleEnable}>
<ha-svg-icon
slot="start"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.enable")}
</ha-md-menu-item>
`
: item.source !== "system"
? html`
<ha-md-menu-item
class="warning"
@click=${this._handleDisable}
graphic="icon"
>
<ha-svg-icon
slot="start"
class="warning"
.path=${mdiStopCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.disable")}
</ha-md-menu-item>
`
: nothing}
${item.source !== "system"
? html`
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
<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>
`
: nothing}
</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>
${item.disabled_by && devices.length
? html`
<ha-md-menu-item
href=${devices.length === 1
? `/config/devices/device/${devices[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.devices`,
{ count: devices.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item>
`
: nothing}
${item.disabled_by && services.length
? html`<ha-md-menu-item
href=${services.length === 1
? `/config/devices/device/${services[0].id}`
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
.path=${mdiHandExtendedOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.services`,
{ count: services.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item> `
: nothing}
${item.disabled_by && entities.length
? html`
<ha-md-menu-item
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
>
<ha-svg-icon
.path=${mdiShapeOutline}
slot="start"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.integrations.config_entry.entities`,
{ count: entities.length }
)}
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-menu-item>
`
: nothing}
${!item.disabled_by &&
RECOVERABLE_STATES.includes(item.state) &&
item.supports_unload &&
item.source !== "system"
? html`
<ha-md-menu-item @click=${this._handleReload}>
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reload"
)}
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item @click=${this._handleRename} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.delete"
"ui.panel.config.integrations.config_entry.rename"
)}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
${this._diagnosticHandler && item.state === "loaded"
? html`
<ha-md-menu-item
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
</ha-md-menu-item>
`
: nothing}
${!item.disabled_by &&
item.supports_reconfigure &&
item.source !== "system"
? html`
<ha-md-menu-item @click=${this._handleReconfigure}>
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.reconfigure"
)}
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item @click=${this._handleSystemOptions} graphic="icon">
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.system_options"
)}
</ha-md-menu-item>
${item.disabled_by === "user"
? html`
<ha-md-menu-item @click=${this._handleEnable}>
<ha-svg-icon
slot="start"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.enable")}
</ha-md-menu-item>
`
: item.source !== "system"
? html`
<ha-md-menu-item
class="warning"
@click=${this._handleDisable}
graphic="icon"
>
<ha-svg-icon
slot="start"
class="warning"
.path=${mdiStopCircleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.disable")}
</ha-md-menu-item>
`
: nothing}
${item.source !== "system"
? html`
<ha-md-menu-item class="warning" @click=${this._handleDelete}>
<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>
`
: nothing}
</ha-md-button-menu>
</ha-md-list-item>`;
}
@@ -1115,7 +1009,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
private _continueFlow(ev) {
showConfigFlowDialog(this, {
continueFlowId: ev.target.flow.flow_id,
navigateToResult: true,
dialogClosedCallback: () => {
// this._handleFlowUpdated();
},
@@ -1137,27 +1030,6 @@ 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() {
if (!this.domain || !isComponentLoaded(this.hass, "diagnostics")) {
return;
@@ -1305,49 +1177,6 @@ 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 {
this._disableIntegration(
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
@@ -1555,7 +1384,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, configEntry.domain),
entryId: configEntry.entry_id,
navigateToResult: true,
});
}
@@ -1626,12 +1454,6 @@ 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 {
return [
haStyle,
@@ -1761,9 +1583,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
pointer-events: none;
content: "";
}
ha-md-list-item.sub-entry {
--md-list-item-leading-space: 50px;
}
a {
text-decoration: none;
}

View File

@@ -207,8 +207,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
supports_remove_device: false,
supports_unload: false,
supports_reconfigure: false,
supported_subentry_types: {},
num_subentries: 0,
pref_disable_new_entities: false,
pref_disable_polling: false,
disabled_by: null,

View File

@@ -279,7 +279,6 @@ class HaDomainIntegrations extends LitElement {
{
startFlowHandler: domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: true,
manifest: await fetchIntegrationManifest(this.hass, domain),
}
);
@@ -296,7 +295,6 @@ class HaDomainIntegrations extends LitElement {
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
{
continueFlowId: flow.flow_id,
navigateToResult: true,
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, flow.handler),
}

View File

@@ -76,8 +76,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@state()
private _statistics?: ZWaveJSControllerStatisticsUpdatedMessage;
private _dialogOpen = false;
protected async firstUpdated() {
if (this.hass) {
await this._fetchData();
@@ -106,17 +104,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
),
subscribeS2Inclusion(this.hass, this.configEntryId, (message) => {
if (!this._dialogOpen) {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
dsk: message.dsk,
onStop: () => {
setTimeout(() => this._fetchData(), 100);
this._dialogOpen = false;
},
});
this._dialogOpen = true;
}
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId,
dsk: message.dsk,
onStop: () => setTimeout(() => this._fetchData(), 100),
});
}),
];
}
@@ -578,17 +570,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
private async _addNodeClicked() {
if (!this._dialogOpen) {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => {
setTimeout(() => this._fetchData(), 100);
this._dialogOpen = false;
},
});
this._dialogOpen = true;
}
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
onStop: () => setTimeout(() => this._fetchData(), 100),
});
}
private async _removeNodeClicked() {

View File

@@ -706,7 +706,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
--data-table-border-width: 0;
}
:host(:not([narrow])) ha-data-table {
height: calc(100vh - 1px - var(--header-height) - 48px);
height: calc(100vh - 1px - var(--header-height));
display: block;
}

View File

@@ -301,6 +301,10 @@ ${type === "object"
direction: var(--direction);
}
.edit-pane .card-content {
user-select: initial;
}
.edit-pane a {
color: var(--primary-color);
}

View File

@@ -20,31 +20,40 @@ export class HuiCardFeatures extends LitElement {
return nothing;
}
return html`
${this.features.map(
(feature) => html`
<hui-card-feature
.hass=${this.hass}
.stateObj=${this.stateObj}
.color=${this.color}
.feature=${feature}
></hui-card-feature>
`
)}
<div class="container">
${this.features.map(
(feature) => html`
<hui-card-feature
.hass=${this.hass}
.stateObj=${this.stateObj}
.color=${this.color}
.feature=${feature}
></hui-card-feature>
`
)}
</div>
`;
}
static styles = css`
:host {
--feature-color: var(--state-icon-color);
--feature-padding: 12px;
--feature-height: 42px;
--feature-border-radius: 12px;
--feature-button-spacing: 12px;
position: relative;
width: 100%;
}
.container {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
padding: var(--feature-padding);
padding-top: 0px;
gap: var(--feature-padding);
width: 100%;
height: 100%;
box-sizing: border-box;
justify-content: space-evenly;
}

View File

@@ -327,19 +327,17 @@ export class HuiEnergyDevicesDetailGraphCard
);
const untrackedConsumption: BarSeriesOption["data"] = [];
Object.keys(consumptionData.total)
.sort((a, b) => Number(a) - Number(b))
.forEach((time) => {
const ts = Number(time);
const value =
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
const dataPoint: number[] = [ts, value];
if (compare) {
dataPoint[2] = dataPoint[0];
dataPoint[0] = compareTransform(new Date(ts)).getTime();
}
untrackedConsumption.push(dataPoint);
});
Object.keys(consumptionData.total).forEach((time) => {
const ts = Number(time);
const value =
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
const dataPoint: number[] = [ts, value];
if (compare) {
dataPoint[2] = dataPoint[0];
dataPoint[0] = compareTransform(new Date(ts)).getTime();
}
untrackedConsumption.push(dataPoint);
});
// random id to always add untracked at the end
const order = Date.now();
const dataset: BarSeriesOption = {
@@ -450,15 +448,7 @@ export class HuiEnergyDevicesDetailGraphCard
});
});
return sorted_devices
.map(
(device) =>
data.find((d) => {
const id = (d.id as string)
.replace(/^compare-/, "") // Remove compare- prefix
.replace(/-\d+$/, ""); // Remove numeric suffix
return id === device;
})!
)
.map((device) => data.find((d) => (d.id as string).includes(device))!)
.filter(Boolean);
}

View File

@@ -27,7 +27,6 @@ import { hasConfigChanged } from "../../common/has-changed";
import type { ECOption } from "../../../../resources/echarts";
import "../../../../components/ha-card";
import { fireEvent } from "../../../../common/dom/fire_event";
import { measureTextWidth } from "../../../../util/text";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -110,11 +109,8 @@ export class HuiEnergyDevicesGraphCard
return `${title}${params.marker} ${params.seriesName}: ${value}`;
}
private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return {
private _createOptions = memoizeOne(
(data: BarSeriesOption[]): ECOption => ({
xAxis: {
type: "value",
name: "kWh",
@@ -128,17 +124,6 @@ export class HuiEnergyDevicesGraphCard
axisLabel: {
formatter: this._getDeviceName.bind(this),
overflow: "truncate",
fontSize: 12,
margin: 5,
width: Math.min(
isMobile ? 100 : 200,
Math.max(
...(data[0]?.data?.map(
(d: any) =>
measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5
) || [])
)
),
},
},
grid: {
@@ -152,8 +137,8 @@ export class HuiEnergyDevicesGraphCard
show: true,
formatter: this._renderTooltip.bind(this),
},
};
});
})
);
private _getDeviceName(statisticId: string): string {
return (

View File

@@ -300,8 +300,6 @@ export class HuiEnergyUsageGraphCard
type: "bar",
stack: "usage",
data: [],
// @ts-expect-error
order: 0,
});
}
@@ -317,8 +315,6 @@ export class HuiEnergyUsageGraphCard
)
);
// @ts-expect-error
datasets.sort((a, b) => a.order - b.order);
fillDataGapsAndRoundCaps(datasets);
this._chartData = datasets;
}
@@ -486,7 +482,7 @@ export class HuiEnergyUsageGraphCard
this._compareStart!
);
Object.entries(combinedData).forEach(([type, sources], idx) => {
Object.entries(combinedData).forEach(([type, sources]) => {
Object.entries(sources).forEach(([statId, source]) => {
const points: BarSeriesOption["data"] = [];
// Process chart data.
@@ -517,13 +513,6 @@ export class HuiEnergyUsageGraphCard
statId,
statisticsMetaData[statId]
),
// @ts-expect-error
order:
type === "used_solar"
? 1
: type === "to_battery"
? Object.keys(combinedData).length
: idx + 2,
barMaxWidth: 50,
itemStyle: {
borderColor: getEnergyColor(

View File

@@ -256,7 +256,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
hui-card-features {
width: 100%;
flex: none;
padding: 0 12px 12px 12px;
}
`;
}

View File

@@ -1,8 +1,4 @@
import {
mdiDotsHexagon,
mdiGoogleCirclesCommunities,
mdiImageFilterCenterFocus,
} from "@mdi/js";
import { mdiImageFilterCenterFocus } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { LatLngTuple } from "leaflet";
import type { PropertyValues } from "lit";
@@ -76,8 +72,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
@state() private _error?: { code: string; message: string };
@state() private _clusterMarkers = true;
private _subscribed?: Promise<(() => Promise<void>) | undefined>;
public setConfig(config: MapCardConfig): void {
@@ -176,32 +170,18 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones}
.themeMode=${themeMode}
.clusterMarkers=${this._clusterMarkers}
interactive-zones
render-passive
></ha-map>
<div id="buttons">
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.toggle_grouping"
)}
.path=${this._clusterMarkers
? mdiGoogleCirclesCommunities
: mdiDotsHexagon}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._toggleClusterMarkers}
tabindex="0"
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
</div>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.map.reset_focus"
)}
.path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap}
tabindex="0"
></ha-icon-button>
</div>
</ha-card>
`;
@@ -340,10 +320,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._map?.fitMap();
}
private _toggleClusterMarkers() {
this._clusterMarkers = !this._clusterMarkers;
}
private _getColor(entityId: string): string {
let color = this._colorDict[entityId];
if (color) {
@@ -488,12 +464,11 @@ class HuiMapCard extends LitElement implements LovelaceCard {
overflow: hidden;
}
#buttons {
ha-icon-button {
position: absolute;
top: 75px;
left: 3px;
display: flex;
flex-direction: column;
outline: none;
}
#root {

View File

@@ -3,21 +3,17 @@ 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 hash from "object-hash";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import "../../../components/ha-card";
import "../../../components/ha-markdown";
import "../../../components/ha-alert";
import type { RenderTemplateResult } from "../../../data/ws-templates";
import { subscribeRenderTemplate } from "../../../data/ws-templates";
import type { HomeAssistant } from "../../../types";
import { CacheManager } from "../../../util/cache-manager";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { MarkdownCardConfig } from "./types";
const templateCache = new CacheManager<RenderTemplateResult>(1000);
@customElement("hui-markdown-card")
export class HuiMarkdownCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@@ -72,32 +68,9 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
this._tryConnect();
}
private _computeCacheKey() {
return hash(this._config);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._tryDisconnect();
if (this._config && this._templateResult) {
const key = this._computeCacheKey();
templateCache.set(key, this._templateResult);
}
}
protected willUpdate(_changedProperties: PropertyValues): void {
super.willUpdate(_changedProperties);
if (!this._config) {
return;
}
if (!this._templateResult) {
const key = this._computeCacheKey();
if (templateCache.has(key)) {
this._templateResult = templateCache.get(key);
}
}
}
protected render() {
@@ -114,7 +87,6 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
: nothing}
<ha-card .header=${this._config.title}>
<ha-markdown
cache
breaks
class=${classMap({
"no-header": !this._config.title,
@@ -135,7 +107,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
this._tryConnect();
}
const shouldBeHidden =
!!this._templateResult &&
this._templateResult &&
this._config.show_empty === false &&
this._templateResult.result.length === 0;
if (shouldBeHidden !== this.hidden) {

View File

@@ -327,7 +327,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
);
const endDate = this._energyEnd;
try {
let unitClass: string | undefined | null;
let unitClass;
if (this._config!.unit && this._metadata) {
const metadata = Object.values(this._metadata).find(
(metaData) =>

Some files were not shown because too many files have changed in this diff Show More