mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-25 03:53:17 +00:00
Compare commits
101 Commits
20241106.0
...
fix_downlo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a5152c36 | ||
|
|
918fca4d0a | ||
|
|
258a19028b | ||
|
|
7b4536564e | ||
|
|
64c260c1c4 | ||
|
|
36f3ef9e86 | ||
|
|
42622fe21e | ||
|
|
64f7afd60f | ||
|
|
d9cd428bf4 | ||
|
|
3282785cf2 | ||
|
|
2c1931adb1 | ||
|
|
c9cad254d2 | ||
|
|
be6ecefb9e | ||
|
|
f4f2cce57e | ||
|
|
99bde50c01 | ||
|
|
a2471f82a3 | ||
|
|
556315b360 | ||
|
|
bed470f79d | ||
|
|
9acf946097 | ||
|
|
231ef4b5b4 | ||
|
|
f8bcc6dde4 | ||
|
|
11ed4600fd | ||
|
|
c0e2d6fa23 | ||
|
|
942562161a | ||
|
|
d35c40b585 | ||
|
|
8cd0ddceb8 | ||
|
|
c3ee49298a | ||
|
|
89dc1a7ebc | ||
|
|
c90e820c7f | ||
|
|
ced70fd9a1 | ||
|
|
253c8f358b | ||
|
|
0c0b657c79 | ||
|
|
8941837697 | ||
|
|
23b55484c3 | ||
|
|
1990b8fa84 | ||
|
|
03ea08f98c | ||
|
|
1f5f6c5f8a | ||
|
|
fa821b1c4f | ||
|
|
f51bc40203 | ||
|
|
c7dae49c42 | ||
|
|
b056b71557 | ||
|
|
0db2b45cc3 | ||
|
|
1be1003549 | ||
|
|
b8a13dd6eb | ||
|
|
cae5540c44 | ||
|
|
d47966cdf7 | ||
|
|
991cf83ff3 | ||
|
|
b83be38514 | ||
|
|
17982e0bdc | ||
|
|
b918862bb1 | ||
|
|
6bdc7af09f | ||
|
|
01adef6d9f | ||
|
|
7cbebfd603 | ||
|
|
4a1adf42b8 | ||
|
|
0c2e62ec91 | ||
|
|
42b1f938d6 | ||
|
|
311f221387 | ||
|
|
3c6be8cf99 | ||
|
|
28703b39da | ||
|
|
db03e271f5 | ||
|
|
7c851d4542 | ||
|
|
4d107f978c | ||
|
|
de57b025e6 | ||
|
|
2218a7121b | ||
|
|
3f4351476f | ||
|
|
d763a014ad | ||
|
|
52a91d8403 | ||
|
|
f6cc435f86 | ||
|
|
349b1ccaad | ||
|
|
ca921be9d2 | ||
|
|
919932e414 | ||
|
|
97a8b6da34 | ||
|
|
1eceaa0d1b | ||
|
|
ba3fae2577 | ||
|
|
93ed1cae5e | ||
|
|
d8618b4a25 | ||
|
|
1f6b0360de | ||
|
|
e1830470b6 | ||
|
|
9e002f7940 | ||
|
|
a1380e93ea | ||
|
|
c511672b0d | ||
|
|
d6d6d1d0b5 | ||
|
|
bee629f7ed | ||
|
|
d8df380edc | ||
|
|
cbfcad71d5 | ||
|
|
327a9ff836 | ||
|
|
ae2c389273 | ||
|
|
5ce75cea0d | ||
|
|
ee79c3a983 | ||
|
|
f396be2ed7 | ||
|
|
9f55ef811d | ||
|
|
4c898a2a5a | ||
|
|
a56e22790d | ||
|
|
2d8fbc652f | ||
|
|
46f0e0212d | ||
|
|
786b9ee8d6 | ||
|
|
1e73cebda6 | ||
|
|
9b9adf3c7a | ||
|
|
a08c7a319f | ||
|
|
5e8868e4b1 | ||
|
|
64285d5155 |
132
.eslintrc.json
132
.eslintrc.json
@@ -1,132 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"airbnb-typescript/base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"modules": true
|
||||
},
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "./webpack.config.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": false,
|
||||
"__DEMO__": false,
|
||||
"__BUILD__": false,
|
||||
"__VERSION__": false,
|
||||
"__STATIC_PATH__": false,
|
||||
"__SUPERVISOR__": false,
|
||||
"Polymer": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"class-methods-use-this": "off",
|
||||
"new-cap": "off",
|
||||
"prefer-template": "off",
|
||||
"object-shorthand": "off",
|
||||
"func-names": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"strict": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-bitwise": "error",
|
||||
"comma-dangle": "off",
|
||||
"vars-on-top": "off",
|
||||
"no-continue": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-multi-assign": "off",
|
||||
"no-console": "error",
|
||||
"radix": "off",
|
||||
"no-alert": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-cycle": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"ts": "never",
|
||||
"js": "never"
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"off",
|
||||
{
|
||||
"selector": "default",
|
||||
"format": ["camelCase", "snake_case"],
|
||||
"leadingUnderscore": "allow",
|
||||
"trailingUnderscore": "allow"
|
||||
},
|
||||
{
|
||||
"selector": ["variable"],
|
||||
"format": ["camelCase", "snake_case", "UPPER_CASE"],
|
||||
"leadingUnderscore": "allow",
|
||||
"trailingUnderscore": "allow"
|
||||
},
|
||||
{
|
||||
"selector": "typeLike",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lit/attribute-names": "warn",
|
||||
"lit/attribute-value-entities": "off",
|
||||
"lit/no-template-map": "off",
|
||||
"lit/no-native-attributes": "warn",
|
||||
"lit/no-this-assign-in-render": "warn",
|
||||
"lit-a11y/click-events-have-key-events": ["off"],
|
||||
"lit-a11y/no-autofocus": "off",
|
||||
"lit-a11y/alt-text": "warn",
|
||||
"lit-a11y/anchor-is-valid": "warn",
|
||||
"lit-a11y/role-has-required-aria-attrs": "warn",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-import-type-side-effects": "error"
|
||||
},
|
||||
"plugins": ["unused-imports"]
|
||||
}
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.9
|
||||
uses: softprops/action-gh-release@v2.1.0
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
with:
|
||||
abi: cp312
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../.eslintrc.json",
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"prefer-arrow-callback": "off"
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ The Home Assistant build pipeline contains various steps to prepare a build.
|
||||
|
||||
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
|
||||
|
||||
We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development.
|
||||
We currently rely on Webpack. Both of these programs bundle the converted files in both production and development.
|
||||
|
||||
For development, bundling is optional. We just want to get the right files in the browser.
|
||||
|
||||
|
||||
@@ -226,13 +226,12 @@ module.exports.config = {
|
||||
return {
|
||||
name: "frontend" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
"service-worker":
|
||||
!env.useRollup() && !latestBuild
|
||||
? {
|
||||
import: "./src/entrypoints/service-worker.ts",
|
||||
layer: "sw",
|
||||
}
|
||||
: "./src/entrypoints/service-worker.ts",
|
||||
"service-worker": !latestBuild
|
||||
? {
|
||||
import: "./src/entrypoints/service-worker.ts",
|
||||
layer: "sw",
|
||||
}
|
||||
: "./src/entrypoints/service-worker.ts",
|
||||
app: "./src/entrypoints/app.ts",
|
||||
authorize: "./src/entrypoints/authorize.ts",
|
||||
onboarding: "./src/entrypoints/onboarding.ts",
|
||||
|
||||
@@ -3,9 +3,6 @@ const path = require("path");
|
||||
const paths = require("./paths.cjs");
|
||||
|
||||
module.exports = {
|
||||
useRollup() {
|
||||
return process.env.ROLLUP === "1";
|
||||
},
|
||||
useWDS() {
|
||||
return process.env.WDS === "1";
|
||||
},
|
||||
|
||||
16
build-scripts/eslint.config.mjs
Normal file
16
build-scripts/eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
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-var-requires": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -6,7 +6,6 @@ import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./locale-data.js";
|
||||
import "./rollup.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./wds.js";
|
||||
@@ -27,11 +26,7 @@ gulp.task(
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-app",
|
||||
env.useWDS()
|
||||
? "wds-watch-app"
|
||||
: env.useRollup()
|
||||
? "rollup-watch-app"
|
||||
: "webpack-watch-app"
|
||||
env.useWDS() ? "wds-watch-app" : "webpack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -44,7 +39,7 @@ gulp.task(
|
||||
"clean",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-app",
|
||||
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
|
||||
"webpack-prod-app",
|
||||
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
|
||||
// Don't compress running tests
|
||||
...(env.isTestBuild() ? [] : ["compress-app"])
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./rollup.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./webpack.js";
|
||||
@@ -19,7 +17,7 @@ gulp.task(
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
"gen-pages-cast-dev",
|
||||
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
|
||||
"webpack-dev-server-cast"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -33,7 +31,7 @@ gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-cast",
|
||||
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
|
||||
"webpack-prod-cast",
|
||||
"gen-pages-cast-prod"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import gulp from "gulp";
|
||||
import env from "../env.cjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./rollup.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./webpack.js";
|
||||
@@ -24,7 +22,7 @@ gulp.task(
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-demo",
|
||||
env.useRollup() ? "rollup-dev-server-demo" : "webpack-dev-server-demo"
|
||||
"webpack-dev-server-demo"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -39,7 +37,7 @@ gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-demo",
|
||||
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
|
||||
"webpack-prod-demo",
|
||||
"gen-pages-demo-prod"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -127,6 +127,7 @@ gulp.task("fetch-lokalise", async function () {
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
})
|
||||
.then((download) => fetch(download.bundle_url))
|
||||
.then((response) => {
|
||||
|
||||
@@ -56,7 +56,6 @@ const getCommonTemplateVars = () => {
|
||||
{ ignorePatch: true, allowHigherVersions: true }
|
||||
);
|
||||
return {
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
||||
};
|
||||
|
||||
@@ -4,13 +4,11 @@ import gulp from "gulp";
|
||||
import yaml from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import path from "path";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./rollup.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
import "./webpack.js";
|
||||
@@ -158,9 +156,7 @@ gulp.task(
|
||||
"copy-static-gallery",
|
||||
"gen-pages-gallery-dev",
|
||||
gulp.parallel(
|
||||
env.useRollup()
|
||||
? "rollup-dev-server-gallery"
|
||||
: "webpack-dev-server-gallery",
|
||||
"webpack-dev-server-gallery",
|
||||
async function watchMarkdownFiles() {
|
||||
gulp.watch(
|
||||
[
|
||||
@@ -189,7 +185,7 @@ gulp.task(
|
||||
"gather-gallery-pages"
|
||||
),
|
||||
"copy-static-gallery",
|
||||
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
|
||||
"webpack-prod-gallery",
|
||||
"gen-pages-gallery-prod"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
import env from "../env.cjs";
|
||||
|
||||
const npmPath = (...parts) =>
|
||||
path.resolve(paths.polymer_dir, "node_modules", ...parts);
|
||||
@@ -69,9 +68,6 @@ function copyPolyfills(staticDir) {
|
||||
}
|
||||
|
||||
function copyLoaderJS(staticDir) {
|
||||
if (!env.useRollup()) {
|
||||
return;
|
||||
}
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js"));
|
||||
copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js"));
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./rollup.js";
|
||||
import "./translations.js";
|
||||
import "./webpack.js";
|
||||
|
||||
@@ -22,7 +21,7 @@ gulp.task(
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
|
||||
"webpack-watch-hassio"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -38,7 +37,7 @@ gulp.task(
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
"copy-static-supervisor",
|
||||
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
|
||||
"webpack-prod-hassio",
|
||||
"gen-pages-hassio-prod",
|
||||
...// Don't compress running tests
|
||||
(env.isTestBuild() ? [] : ["compress-hassio"])
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
// Tasks to run Rollup
|
||||
|
||||
import log from "fancy-log";
|
||||
import gulp from "gulp";
|
||||
import http from "http";
|
||||
import open from "open";
|
||||
import path from "path";
|
||||
import { rollup } from "rollup";
|
||||
import handler from "serve-handler";
|
||||
import paths from "../paths.cjs";
|
||||
import rollupConfig from "../rollup.cjs";
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) =>
|
||||
gulp.series(
|
||||
async function buildLatest() {
|
||||
await buildRollup(
|
||||
createConfigFunc({
|
||||
...params,
|
||||
latestBuild: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
async function buildES5() {
|
||||
await buildRollup(
|
||||
createConfigFunc({
|
||||
...params,
|
||||
latestBuild: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function createServer(serveOptions) {
|
||||
const server = http.createServer((request, response) =>
|
||||
handler(request, response, {
|
||||
public: serveOptions.root,
|
||||
})
|
||||
);
|
||||
|
||||
server.listen(
|
||||
serveOptions.port,
|
||||
serveOptions.networkAccess ? "0.0.0.0" : undefined,
|
||||
() => {
|
||||
log.info(`Available at http://localhost:${serveOptions.port}`);
|
||||
open(`http://localhost:${serveOptions.port}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function watchRollup(createConfig, extraWatchSrc = [], serveOptions = null) {
|
||||
const { inputOptions, outputOptions } = createConfig({
|
||||
isProdBuild: false,
|
||||
latestBuild: true,
|
||||
});
|
||||
|
||||
const watcher = rollup.watch({
|
||||
...inputOptions,
|
||||
output: [outputOptions],
|
||||
watch: {
|
||||
include: ["src/**"] + extraWatchSrc,
|
||||
},
|
||||
});
|
||||
|
||||
let startedHttp = false;
|
||||
|
||||
watcher.on("event", (event) => {
|
||||
if (event.code === "BUNDLE_END") {
|
||||
log(`Build done @ ${new Date().toLocaleTimeString()}`);
|
||||
} else if (event.code === "ERROR") {
|
||||
log.error(event.error);
|
||||
} else if (event.code === "END") {
|
||||
if (startedHttp || !serveOptions) {
|
||||
return;
|
||||
}
|
||||
startedHttp = true;
|
||||
createServer(serveOptions);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
);
|
||||
}
|
||||
|
||||
async function buildRollup(config) {
|
||||
const bundle = await rollup.rollup(config.inputOptions);
|
||||
await bundle.write(config.outputOptions);
|
||||
}
|
||||
|
||||
gulp.task("rollup-watch-app", () => {
|
||||
watchRollup(rollupConfig.createAppConfig);
|
||||
});
|
||||
|
||||
gulp.task("rollup-watch-hassio", () => {
|
||||
watchRollup(rollupConfig.createHassioConfig, ["hassio/src/**"]);
|
||||
});
|
||||
|
||||
gulp.task("rollup-dev-server-demo", () => {
|
||||
watchRollup(rollupConfig.createDemoConfig, ["demo/src/**"], {
|
||||
root: paths.demo_output_root,
|
||||
port: 8090,
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("rollup-dev-server-cast", () => {
|
||||
watchRollup(rollupConfig.createCastConfig, ["cast/src/**"], {
|
||||
root: paths.cast_output_root,
|
||||
port: 8080,
|
||||
networkAccess: true,
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("rollup-dev-server-gallery", () => {
|
||||
watchRollup(rollupConfig.createGalleryConfig, ["gallery/src/**"], {
|
||||
root: paths.gallery_output_root,
|
||||
port: 8100,
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"rollup-prod-app",
|
||||
bothBuilds(rollupConfig.createAppConfig, { isProdBuild: true })
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"rollup-prod-demo",
|
||||
bothBuilds(rollupConfig.createDemoConfig, { isProdBuild: true })
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"rollup-prod-cast",
|
||||
bothBuilds(rollupConfig.createCastConfig, { isProdBuild: true })
|
||||
);
|
||||
|
||||
gulp.task("rollup-prod-hassio", () =>
|
||||
bothBuilds(rollupConfig.createHassioConfig, { isProdBuild: true })
|
||||
);
|
||||
|
||||
gulp.task("rollup-prod-gallery", () =>
|
||||
buildRollup(
|
||||
rollupConfig.createGalleryConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = function (opts = {}) {
|
||||
const dontHash = opts.dontHash || new Set();
|
||||
|
||||
return {
|
||||
name: "dont-hash",
|
||||
renderChunk(_code, chunk, _options) {
|
||||
if (!chunk.isEntry || !dontHash.has(chunk.name)) {
|
||||
return null;
|
||||
}
|
||||
chunk.fileName = `${chunk.name}.js`;
|
||||
return null;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
module.exports = function (userOptions = {}) {
|
||||
// Files need to be absolute paths.
|
||||
// This only works if the file has no exports
|
||||
// and only is imported for its side effects
|
||||
const files = userOptions.files || [];
|
||||
|
||||
if (files.length === 0) {
|
||||
return {
|
||||
name: "ignore",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: "ignore",
|
||||
|
||||
load(id) {
|
||||
return files.some((toIgnorePath) => id.startsWith(toIgnorePath))
|
||||
? {
|
||||
code: "",
|
||||
}
|
||||
: null;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
const url = require("url");
|
||||
|
||||
const defaultOptions = {
|
||||
publicPath: "",
|
||||
};
|
||||
|
||||
module.exports = function (userOptions = {}) {
|
||||
const options = { ...defaultOptions, ...userOptions };
|
||||
|
||||
return {
|
||||
name: "manifest",
|
||||
generateBundle(outputOptions, bundle) {
|
||||
const manifest = {};
|
||||
|
||||
for (const chunk of Object.values(bundle)) {
|
||||
if (!chunk.isEntry) {
|
||||
continue;
|
||||
}
|
||||
// Add js extension to mimic Webpack manifest.
|
||||
manifest[`${chunk.name}.js`] = url.resolve(
|
||||
options.publicPath,
|
||||
chunk.fileName
|
||||
);
|
||||
}
|
||||
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
source: JSON.stringify(manifest, undefined, 2),
|
||||
name: "manifest.json",
|
||||
fileName: "manifest.json",
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
// Worker plugin
|
||||
// Each worker will include all of its dependencies
|
||||
// instead of relying on an importer.
|
||||
|
||||
// Forked from v.1.4.1
|
||||
// https://github.com/surma/rollup-plugin-off-main-thread
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const rollup = require("rollup");
|
||||
const path = require("path");
|
||||
const MagicString = require("magic-string");
|
||||
|
||||
const defaultOpts = {
|
||||
// A RegExp to find `new Workers()` calls. The second capture group _must_
|
||||
// capture the provided file name without the quotes.
|
||||
workerRegexp: /new Worker\((["'])(.+?)\1(,[^)]+)?\)/g,
|
||||
plugins: ["node-resolve", "commonjs", "babel", "terser", "ignore"],
|
||||
};
|
||||
|
||||
async function getBundledWorker(workerPath, rollupOptions) {
|
||||
const bundle = await rollup.rollup({
|
||||
...rollupOptions,
|
||||
input: {
|
||||
worker: workerPath,
|
||||
},
|
||||
});
|
||||
const { output } = await bundle.generate({
|
||||
// Generates cleanest output, we shouldn't have any imports/exports
|
||||
// that would be incompatible with ES5.
|
||||
format: "es",
|
||||
// We should not export anything. This will fail build if we are.
|
||||
exports: "none",
|
||||
});
|
||||
|
||||
let code;
|
||||
|
||||
for (const chunkOrAsset of output) {
|
||||
if (chunkOrAsset.name === "worker") {
|
||||
code = chunkOrAsset.code;
|
||||
} else if (chunkOrAsset.type !== "asset") {
|
||||
throw new Error("Unexpected extra output");
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
module.exports = function (opts = {}) {
|
||||
opts = { ...defaultOpts, ...opts };
|
||||
|
||||
let rollupOptions;
|
||||
let refIds;
|
||||
|
||||
return {
|
||||
name: "hass-worker",
|
||||
|
||||
async buildStart(options) {
|
||||
refIds = {};
|
||||
rollupOptions = {
|
||||
plugins: options.plugins.filter((plugin) =>
|
||||
opts.plugins.includes(plugin.name)
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
async transform(code, id) {
|
||||
// Copy the regexp as they are stateful and this hook is async.
|
||||
const workerRegexp = new RegExp(
|
||||
opts.workerRegexp.source,
|
||||
opts.workerRegexp.flags
|
||||
);
|
||||
if (!workerRegexp.test(code)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const ms = new MagicString(code);
|
||||
// Reset the regexp
|
||||
workerRegexp.lastIndex = 0;
|
||||
for (;;) {
|
||||
const match = workerRegexp.exec(code);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
|
||||
const workerFile = match[2];
|
||||
let optionsObject = {};
|
||||
// Parse the optional options object
|
||||
if (match[3] && match[3].length > 0) {
|
||||
// FIXME: ooooof!
|
||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
||||
optionsObject = new Function(`return ${match[3].slice(1)};`)();
|
||||
}
|
||||
delete optionsObject.type;
|
||||
|
||||
if (!/^.*\//.test(workerFile)) {
|
||||
this.warn(
|
||||
`Paths passed to the Worker constructor must be relative or absolute, i.e. start with /, ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find worker file and store it as a chunk with ID prefixed for our loader
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resolvedWorkerFile = (await this.resolve(workerFile, id)).id;
|
||||
let chunkRefId;
|
||||
if (resolvedWorkerFile in refIds) {
|
||||
chunkRefId = refIds[resolvedWorkerFile];
|
||||
} else {
|
||||
this.addWatchFile(resolvedWorkerFile);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const source = await getBundledWorker(
|
||||
resolvedWorkerFile,
|
||||
rollupOptions
|
||||
);
|
||||
chunkRefId = refIds[resolvedWorkerFile] = this.emitFile({
|
||||
name: path.basename(resolvedWorkerFile),
|
||||
source,
|
||||
type: "asset",
|
||||
});
|
||||
}
|
||||
|
||||
const workerParametersStartIndex = match.index + "new Worker(".length;
|
||||
const workerParametersEndIndex =
|
||||
match.index + match[0].length - ")".length;
|
||||
|
||||
ms.overwrite(
|
||||
workerParametersStartIndex,
|
||||
workerParametersEndIndex,
|
||||
`import.meta.ROLLUP_FILE_URL_${chunkRefId}, ${JSON.stringify(
|
||||
optionsObject
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
code: ms.toString(),
|
||||
map: ms.generateMap({ hires: true }),
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
const commonjs = require("@rollup/plugin-commonjs");
|
||||
const resolve = require("@rollup/plugin-node-resolve");
|
||||
const json = require("@rollup/plugin-json");
|
||||
const { babel } = require("@rollup/plugin-babel");
|
||||
const replace = require("@rollup/plugin-replace");
|
||||
const visualizer = require("rollup-plugin-visualizer");
|
||||
const { string } = require("rollup-plugin-string");
|
||||
const { terser } = require("rollup-plugin-terser");
|
||||
const manifest = require("./rollup-plugins/manifest-plugin.cjs");
|
||||
const worker = require("./rollup-plugins/worker-plugin.cjs");
|
||||
const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin.cjs");
|
||||
const ignore = require("./rollup-plugins/ignore-plugin.cjs");
|
||||
|
||||
const bundle = require("./bundle.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
|
||||
const extensions = [".js", ".ts"];
|
||||
|
||||
/**
|
||||
* @param {Object} arg
|
||||
* @param { import("rollup").InputOption } arg.input
|
||||
*/
|
||||
const createRollupConfig = ({
|
||||
entry,
|
||||
outputPath,
|
||||
defineOverlay,
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
publicPath,
|
||||
dontHash,
|
||||
isWDS,
|
||||
}) => ({
|
||||
/**
|
||||
* @type { import("rollup").InputOptions }
|
||||
*/
|
||||
inputOptions: {
|
||||
input: entry,
|
||||
// Some entry points contain no JavaScript. This setting silences a warning about that.
|
||||
// https://rollupjs.org/configuration-options/#preserveentrysignatures
|
||||
preserveEntrySignatures: false,
|
||||
plugins: [
|
||||
ignore({
|
||||
files: bundle
|
||||
.emptyPackages({ latestBuild })
|
||||
// TEMP HACK: Makes Rollup build work again
|
||||
.concat(
|
||||
require.resolve(
|
||||
"@webcomponents/scoped-custom-element-registry/scoped-custom-element-registry.min"
|
||||
)
|
||||
),
|
||||
}),
|
||||
resolve({
|
||||
extensions,
|
||||
preferBuiltins: false,
|
||||
browser: true,
|
||||
rootDir: paths.polymer_dir,
|
||||
}),
|
||||
commonjs(),
|
||||
json(),
|
||||
babel({
|
||||
...bundle.babelOptions({ latestBuild, isProdBuild }),
|
||||
extensions,
|
||||
babelHelpers: isWDS ? "inline" : "bundled",
|
||||
}),
|
||||
string({
|
||||
// Import certain extensions as strings
|
||||
include: [path.join(paths.polymer_dir, "node_modules/**/*.css")],
|
||||
}),
|
||||
replace(bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })),
|
||||
!isWDS &&
|
||||
manifest({
|
||||
publicPath,
|
||||
}),
|
||||
!isWDS && worker(),
|
||||
!isWDS && dontHashPlugin({ dontHash }),
|
||||
!isWDS && isProdBuild && terser(bundle.terserOptions({ latestBuild })),
|
||||
!isWDS &&
|
||||
isStatsBuild &&
|
||||
visualizer({
|
||||
// https://github.com/btd/rollup-plugin-visualizer#options
|
||||
open: true,
|
||||
sourcemap: true,
|
||||
}),
|
||||
].filter(Boolean),
|
||||
},
|
||||
/**
|
||||
* @type { import("rollup").OutputOptions }
|
||||
*/
|
||||
outputOptions: {
|
||||
// https://rollupjs.org/configuration-options/#output-dir
|
||||
dir: outputPath,
|
||||
// https://rollupjs.org/configuration-options/#output-format
|
||||
format: latestBuild ? "es" : "systemjs",
|
||||
// https://rollupjs.org/configuration-options/#output-externallivebindings
|
||||
externalLiveBindings: false,
|
||||
// https://rollupjs.org/configuration-options/#output-entryfilenames
|
||||
// https://rollupjs.org/configuration-options/#output-chunkfilenames
|
||||
// https://rollupjs.org/configuration-options/#output-assetfilenames
|
||||
entryFileNames:
|
||||
isProdBuild && !isStatsBuild ? "[name]-[hash].js" : "[name].js",
|
||||
chunkFileNames: isProdBuild && !isStatsBuild ? "c.[hash].js" : "[name].js",
|
||||
assetFileNames: isProdBuild && !isStatsBuild ? "a.[hash].js" : "[name].js",
|
||||
// https://rollupjs.org/configuration-options/#output-sourcemap
|
||||
sourcemap: isProdBuild ? true : "inline",
|
||||
},
|
||||
});
|
||||
|
||||
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) =>
|
||||
createRollupConfig(
|
||||
bundle.config.app({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isWDS,
|
||||
})
|
||||
);
|
||||
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
createRollupConfig(
|
||||
bundle.config.demo({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
})
|
||||
);
|
||||
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRollupConfig(bundle.config.cast({ isProdBuild, latestBuild }));
|
||||
|
||||
const createHassioConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRollupConfig(bundle.config.hassio({ isProdBuild, latestBuild }));
|
||||
|
||||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRollupConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createRollupConfig,
|
||||
};
|
||||
@@ -188,6 +188,7 @@ const createWebpackConfig = ({
|
||||
"lit/directives/cache$": "lit/directives/cache.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import rollup from "../build-scripts/rollup.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
const config = rollup.createCastConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
});
|
||||
|
||||
export default { ...config.inputOptions, output: config.outputOptions };
|
||||
@@ -1,10 +0,0 @@
|
||||
import rollup from "../build-scripts/rollup.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
const config = rollup.createDemoConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
});
|
||||
|
||||
export default { ...config.inputOptions, output: config.outputOptions };
|
||||
163
eslint.config.mjs
Normal file
163
eslint.config.mjs
Normal file
@@ -0,0 +1,163 @@
|
||||
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";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
...compat.extends(
|
||||
"airbnb-base",
|
||||
"airbnb-typescript/base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:wc/recommended",
|
||||
"plugin:lit/all",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"prettier"
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
__DEV__: false,
|
||||
__DEMO__: false,
|
||||
__BUILD__: false,
|
||||
__VERSION__: false,
|
||||
__STATIC_PATH__: false,
|
||||
__SUPERVISOR__: false,
|
||||
Polymer: true,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
},
|
||||
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: "./webpack.config.cjs",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"class-methods-use-this": "off",
|
||||
"new-cap": "off",
|
||||
"prefer-template": "off",
|
||||
"object-shorthand": "off",
|
||||
"func-names": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
strict: "off",
|
||||
"no-plusplus": "off",
|
||||
"no-bitwise": "error",
|
||||
"comma-dangle": "off",
|
||||
"vars-on-top": "off",
|
||||
"no-continue": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-multi-assign": "off",
|
||||
"no-console": "error",
|
||||
radix: "off",
|
||||
"no-alert": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-cycle": "off",
|
||||
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
ts: "never",
|
||||
js: "never",
|
||||
},
|
||||
],
|
||||
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"off",
|
||||
{
|
||||
selector: "default",
|
||||
format: ["camelCase", "snake_case"],
|
||||
leadingUnderscore: "allow",
|
||||
trailingUnderscore: "allow",
|
||||
},
|
||||
{
|
||||
selector: ["variable"],
|
||||
format: ["camelCase", "snake_case", "UPPER_CASE"],
|
||||
leadingUnderscore: "allow",
|
||||
trailingUnderscore: "allow",
|
||||
},
|
||||
{
|
||||
selector: "typeLike",
|
||||
format: ["PascalCase"],
|
||||
},
|
||||
],
|
||||
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
|
||||
"unused-imports/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
vars: "all",
|
||||
varsIgnorePattern: "^_",
|
||||
args: "after-used",
|
||||
argsIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lit/attribute-names": "warn",
|
||||
"lit/attribute-value-entities": "off",
|
||||
"lit/no-template-map": "off",
|
||||
"lit/no-native-attributes": "warn",
|
||||
"lit/no-this-assign-in-render": "warn",
|
||||
"lit-a11y/click-events-have-key-events": ["off"],
|
||||
"lit-a11y/no-autofocus": "off",
|
||||
"lit-a11y/alt-text": "warn",
|
||||
"lit-a11y/anchor-is-valid": "warn",
|
||||
"lit-a11y/role-has-required-aria-attrs": "warn",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "../.eslintrc.json",
|
||||
"rules": {
|
||||
"no-console": 0
|
||||
}
|
||||
}
|
||||
10
gallery/eslint.config.mjs
Normal file
10
gallery/eslint.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
import rollup from "../build-scripts/rollup.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
const config = rollup.createGalleryConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
});
|
||||
|
||||
export default { ...config.inputOptions, output: config.outputOptions };
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable lit/no-template-arrow */
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
|
||||
@@ -510,6 +510,7 @@ class DemoHaForm extends LitElement {
|
||||
.computeError=${(error) => translations[error] || error}
|
||||
.computeLabel=${(schema) =>
|
||||
translations[schema.name] || schema.name}
|
||||
.computeHelper=${() => "Helper text"}
|
||||
@value-changed=${(e) => {
|
||||
this.data[idx] = e.detail.value;
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import rollup from "../build-scripts/rollup.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
const config = rollup.createHassioConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
latestBuild: false,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
});
|
||||
|
||||
export default { ...config.inputOptions, output: config.outputOptions };
|
||||
@@ -47,7 +47,6 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.header=${this.addon.name}
|
||||
.provider=${this.addon.slug}
|
||||
show
|
||||
.filter=${this._filter}
|
||||
>
|
||||
</error-log-card>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"eslint --flag unstable_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer --quiet",
|
||||
],
|
||||
|
||||
61
package.json
61
package.json
@@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-path=.gitignore",
|
||||
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-path=.gitignore --fix",
|
||||
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore",
|
||||
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
@@ -27,22 +27,22 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.2",
|
||||
"@codemirror/autocomplete": "6.18.3",
|
||||
"@codemirror/commands": "6.7.1",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/legacy-modes": "6.4.2",
|
||||
"@codemirror/search": "6.5.7",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.34.1",
|
||||
"@codemirror/view": "6.34.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.16.1",
|
||||
"@formatjs/intl-displaynames": "6.8.1",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.1",
|
||||
"@formatjs/intl-listformat": "7.7.1",
|
||||
"@formatjs/intl-locale": "4.2.1",
|
||||
"@formatjs/intl-numberformat": "8.14.1",
|
||||
"@formatjs/intl-pluralrules": "5.3.1",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.1",
|
||||
"@formatjs/intl-datetimeformat": "6.16.4",
|
||||
"@formatjs/intl-displaynames": "6.8.4",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.2",
|
||||
"@formatjs/intl-listformat": "7.7.4",
|
||||
"@formatjs/intl-locale": "4.2.4",
|
||||
"@formatjs/intl-numberformat": "8.14.4",
|
||||
"@formatjs/intl-pluralrules": "5.3.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.4",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.5.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.5.1",
|
||||
"@vaadin/combo-box": "24.5.3",
|
||||
"@vaadin/vaadin-themable-mixin": "24.5.3",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -98,10 +98,10 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "2.2.11",
|
||||
"barcode-detector": "2.3.1",
|
||||
"chart.js": "4.4.6",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.39.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -115,13 +115,13 @@
|
||||
"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.3",
|
||||
"intl-messageformat": "10.7.6",
|
||||
"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",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "14.1.3",
|
||||
"marked": "15.0.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -153,25 +153,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.25.9",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.16.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.17.0",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.8.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.2",
|
||||
"@octokit/rest": "21.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.4",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.17",
|
||||
"@types/chromecast-caf-receiver": "6.0.18",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -191,13 +186,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.2.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.2",
|
||||
"del": "8.0.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint": "9.15.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
@@ -230,10 +224,6 @@
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"rollup": "2.79.2",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.6",
|
||||
"sinon": "19.0.2",
|
||||
"systemjs": "6.15.1",
|
||||
@@ -247,7 +237,7 @@
|
||||
"webpack-dev-server": "5.1.0",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
@@ -257,7 +247,8 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.15"
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"globals": "15.12.0"
|
||||
},
|
||||
"packageManager": "yarn@4.5.1"
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import rollup from "../build-scripts/rollup.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
const config = rollup.createAppConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
});
|
||||
|
||||
export default { ...config.inputOptions, output: config.outputOptions };
|
||||
@@ -3,6 +3,7 @@ import "@material/mwc-button";
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
@@ -224,16 +225,19 @@ export class HaAuthFlow extends LitElement {
|
||||
: this.localize("ui.panel.page-authorize.just_checking")}
|
||||
</h1>
|
||||
${this._computeStepDescription(step)}
|
||||
<ha-auth-form
|
||||
.localize=${this.localize}
|
||||
.data=${this._stepData!}
|
||||
.schema=${autocompleteLoginFields(step.data_schema)}
|
||||
.error=${step.errors}
|
||||
.disabled=${this._submitting}
|
||||
.computeLabel=${this._computeLabelCallback(step)}
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-auth-form>
|
||||
${keyed(
|
||||
step.step_id,
|
||||
html`<ha-auth-form
|
||||
.localize=${this.localize}
|
||||
.data=${this._stepData!}
|
||||
.schema=${autocompleteLoginFields(step.data_schema)}
|
||||
.error=${step.errors}
|
||||
.disabled=${this._submitting}
|
||||
.computeLabel=${this._computeLabelCallback(step)}
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-auth-form>`
|
||||
)}
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
|
||||
@@ -54,6 +54,7 @@ export class HaAuthFormString extends HaFormString {
|
||||
.autoValidate=${this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
?autofocus=${this.schema.autofocus}
|
||||
.suffix=${
|
||||
this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
|
||||
@@ -69,6 +69,7 @@ export class HaAuthTextField extends HaTextField {
|
||||
name=${ifDefined(this.name === "" ? undefined : this.name)}
|
||||
inputmode=${ifDefined(this.inputMode)}
|
||||
autocapitalize=${ifDefined(autocapitalizeOrUndef)}
|
||||
?autofocus=${this.autofocus}
|
||||
@input=${this.handleInputChange}
|
||||
@focus=${this.onInputFocus}
|
||||
@blur=${this.onInputBlur}
|
||||
@@ -246,6 +247,14 @@ export class HaAuthTextField extends HaTextField {
|
||||
this.append(style);
|
||||
return this;
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
super.firstUpdated();
|
||||
|
||||
if (this.autofocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatListWithAnds } from "../string/format-list";
|
||||
|
||||
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
||||
|
||||
@@ -42,3 +43,62 @@ export const formatDuration = (
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const formatDurationLong = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => {
|
||||
const d = duration.days || 0;
|
||||
const h = duration.hours || 0;
|
||||
const m = duration.minutes || 0;
|
||||
const s = duration.seconds || 0;
|
||||
const ms = duration.milliseconds || 0;
|
||||
|
||||
const parts: string[] = [];
|
||||
if (d > 0) {
|
||||
parts.push(
|
||||
Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "day",
|
||||
unitDisplay: "long",
|
||||
}).format(d)
|
||||
);
|
||||
}
|
||||
if (h > 0) {
|
||||
parts.push(
|
||||
Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "hour",
|
||||
unitDisplay: "long",
|
||||
}).format(h)
|
||||
);
|
||||
}
|
||||
if (m > 0) {
|
||||
parts.push(
|
||||
Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
unitDisplay: "long",
|
||||
}).format(m)
|
||||
);
|
||||
}
|
||||
if (s > 0) {
|
||||
parts.push(
|
||||
Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "second",
|
||||
unitDisplay: "long",
|
||||
}).format(s)
|
||||
);
|
||||
}
|
||||
if (ms > 0) {
|
||||
parts.push(
|
||||
Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: "millisecond",
|
||||
unitDisplay: "long",
|
||||
}).format(ms)
|
||||
);
|
||||
}
|
||||
return formatListWithAnds(locale, parts);
|
||||
};
|
||||
|
||||
91
src/common/entity/delete_entity.ts
Normal file
91
src/common/entity/delete_entity.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { IntegrationManifest } from "../../data/integration";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { deleteConfigEntry } from "../../data/config_entries";
|
||||
|
||||
export const isDeletableEntity = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
manifests: IntegrationManifest[],
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
configEntries: ConfigEntry[],
|
||||
fetchedHelpers: Helper[]
|
||||
): boolean => {
|
||||
const restored = !!hass.states[entity_id]?.attributes.restored;
|
||||
if (restored) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const domain = computeDomain(entity_id);
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
return !!(
|
||||
isComponentLoaded(hass, domain) &&
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
);
|
||||
}
|
||||
|
||||
const configEntryId = entityRegEntry?.config_entry_id;
|
||||
if (!configEntryId) {
|
||||
return false;
|
||||
}
|
||||
const configEntry = configEntries.find((e) => e.entry_id === configEntryId);
|
||||
return (
|
||||
manifests.find((m) => m.domain === configEntry?.domain)
|
||||
?.integration_type === "helper"
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteEntity = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
manifests: IntegrationManifest[],
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
configEntries: ConfigEntry[],
|
||||
fetchedHelpers: Helper[]
|
||||
) => {
|
||||
// This function assumes the entity_id already was validated by isDeletableEntity and does not repeat all those checks.
|
||||
const domain = computeDomain(entity_id);
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
if (isComponentLoaded(hass, domain)) {
|
||||
if (
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
) {
|
||||
HELPERS_CRUD[domain].delete(hass, entityRegEntry.unique_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const stateObj = hass.states[entity_id];
|
||||
if (!stateObj?.attributes.restored) {
|
||||
return;
|
||||
}
|
||||
removeEntityRegistryEntry(hass, entity_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const configEntryId = entityRegEntry?.config_entry_id;
|
||||
const configEntry = configEntryId
|
||||
? configEntries.find((e) => e.entry_id === configEntryId)
|
||||
: undefined;
|
||||
const isHelperEntryType = configEntry
|
||||
? manifests.find((m) => m.domain === configEntry.domain)
|
||||
?.integration_type === "helper"
|
||||
: false;
|
||||
|
||||
if (isHelperEntryType) {
|
||||
deleteConfigEntry(hass, configEntryId!);
|
||||
return;
|
||||
}
|
||||
|
||||
removeEntityRegistryEntry(hass, entity_id);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ export const blankBeforePercent = (
|
||||
localeOptions: FrontendLocaleData
|
||||
): string => {
|
||||
switch (localeOptions.language) {
|
||||
case "cz":
|
||||
case "cs":
|
||||
case "de":
|
||||
case "fi":
|
||||
case "fr":
|
||||
|
||||
@@ -16,6 +16,8 @@ export type LocalizeKeys =
|
||||
| `ui.card.lawn_mower.actions.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
| `ui.components.logbook.messages.cleared_device_classes.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||
|
||||
@@ -72,6 +72,12 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property() public chartType: ChartType = "line";
|
||||
|
||||
@property({ type: Number }) public minYAxis?: number;
|
||||
|
||||
@property({ type: Number }) public maxYAxis?: number;
|
||||
|
||||
@property({ type: Boolean }) public fitYData = false;
|
||||
|
||||
@property({ type: Boolean }) public hideLegend = false;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
@@ -113,6 +119,9 @@ export class StatisticsChart extends LitElement {
|
||||
changedProps.has("unit") ||
|
||||
changedProps.has("period") ||
|
||||
changedProps.has("chartType") ||
|
||||
changedProps.has("minYAxis") ||
|
||||
changedProps.has("maxYAxis") ||
|
||||
changedProps.has("fitYData") ||
|
||||
changedProps.has("logarithmicScale") ||
|
||||
changedProps.has("hideLegend")
|
||||
) {
|
||||
@@ -232,6 +241,8 @@ export class StatisticsChart extends LitElement {
|
||||
text: unit || this.unit,
|
||||
},
|
||||
type: this.logarithmicScale ? "logarithmic" : "linear",
|
||||
min: this.fitYData ? null : this.minYAxis,
|
||||
max: this.fitYData ? null : this.maxYAxis,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
|
||||
@@ -113,7 +113,6 @@ class HaDataTableLabels extends LitElement {
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
outline: 1px solid var(--outline-color);
|
||||
}
|
||||
ha-button-menu {
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -8,7 +8,7 @@ export class HaCircularProgress extends MdCircularProgress {
|
||||
@property({ attribute: "aria-label", type: String }) public ariaLabel =
|
||||
"Loading";
|
||||
|
||||
@property() public size: "tiny" | "small" | "medium" | "large" = "medium";
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
@@ -21,7 +21,6 @@ export class HaCircularProgress extends MdCircularProgress {
|
||||
case "small":
|
||||
this.style.setProperty("--md-circular-progress-size", "28px");
|
||||
break;
|
||||
// medium is default size
|
||||
case "medium":
|
||||
this.style.setProperty("--md-circular-progress-size", "48px");
|
||||
break;
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
differenceInMilliseconds,
|
||||
addMilliseconds,
|
||||
subMilliseconds,
|
||||
roundToNearestHours,
|
||||
} from "date-fns";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -30,6 +34,8 @@ import "./date-range-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import "./ha-icon-button-next";
|
||||
import "./ha-icon-button-prev";
|
||||
|
||||
export interface DateRangePickerRanges {
|
||||
[key: string]: [Date, Date];
|
||||
@@ -249,6 +255,12 @@ export class HaDateRangePicker extends LitElement {
|
||||
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
|
||||
${!this.minimal
|
||||
? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-icon-button-prev
|
||||
.label=${this.hass.localize("ui.common.previous")}
|
||||
class="prev"
|
||||
@click=${this._handlePrev}
|
||||
>
|
||||
</ha-icon-button-prev>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(
|
||||
@@ -286,7 +298,13 @@ export class HaDateRangePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleInputClick}
|
||||
readonly
|
||||
></ha-textfield>`
|
||||
></ha-textfield>
|
||||
<ha-icon-button-next
|
||||
.label=${this.hass.localize("ui.common.next")}
|
||||
class="next"
|
||||
@click=${this._handleNext}
|
||||
>
|
||||
</ha-icon-button-next>`
|
||||
: html`<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.select_date_range"
|
||||
@@ -317,6 +335,45 @@ export class HaDateRangePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleNext(): void {
|
||||
const dateRange = [
|
||||
roundToNearestHours(this.endDate),
|
||||
subMilliseconds(
|
||||
roundToNearestHours(
|
||||
addMilliseconds(
|
||||
this.endDate,
|
||||
Math.max(
|
||||
3600000,
|
||||
differenceInMilliseconds(this.endDate, this.startDate)
|
||||
)
|
||||
)
|
||||
),
|
||||
1
|
||||
),
|
||||
];
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
dateRangePicker.clickRange(dateRange);
|
||||
dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
private _handlePrev(): void {
|
||||
const dateRange = [
|
||||
roundToNearestHours(
|
||||
subMilliseconds(
|
||||
this.startDate,
|
||||
Math.max(
|
||||
3600000,
|
||||
differenceInMilliseconds(this.endDate, this.startDate)
|
||||
)
|
||||
)
|
||||
),
|
||||
subMilliseconds(roundToNearestHours(this.startDate), 1),
|
||||
];
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
dateRangePicker.clickRange(dateRange);
|
||||
dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||
const dateRange = Object.values(this.ranges || this._ranges!)[
|
||||
ev.detail.index
|
||||
@@ -418,7 +475,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
min-width: inherit;
|
||||
}
|
||||
|
||||
ha-svg-icon {
|
||||
ha-svg-icon,
|
||||
.prev,
|
||||
.next {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@material/mwc-formfield";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
@@ -19,6 +19,8 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-checkbox", true) private _input?: HTMLElement;
|
||||
@@ -37,6 +39,12 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-checkbox>
|
||||
<span slot="label">
|
||||
<p class="primary">${this.label}</p>
|
||||
${this.helper
|
||||
? html`<p class="secondary">${this.helper}</p>`
|
||||
: nothing}
|
||||
</span>
|
||||
</mwc-formfield>
|
||||
`;
|
||||
}
|
||||
@@ -46,6 +54,28 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
|
||||
value: (ev.target as HaCheckbox).checked,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-formfield {
|
||||
display: flex;
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
--mdc-typography-body2-font-size: 1em;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.secondary {
|
||||
direction: var(--direction);
|
||||
padding-top: 4px;
|
||||
box-sizing: border-box;
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface HaFormStringSchema extends HaFormBaseSchema {
|
||||
type: "string";
|
||||
format?: string;
|
||||
autocomplete?: string;
|
||||
autofocus?: boolean;
|
||||
}
|
||||
|
||||
export interface HaFormBooleanSchema extends HaFormBaseSchema {
|
||||
|
||||
@@ -2,9 +2,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import "./ha-icon-button";
|
||||
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
@@ -20,7 +18,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public rows = 8;
|
||||
|
||||
@property({ attribute: false }) public columns = 4;
|
||||
@property({ attribute: false }) public columns = 12;
|
||||
|
||||
@property({ attribute: false }) public rowMin?: number;
|
||||
|
||||
@@ -32,6 +30,8 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public isDefault?: boolean;
|
||||
|
||||
@property({ attribute: false }) public step: number = 1;
|
||||
|
||||
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
@@ -51,8 +51,9 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
const rowMin = this.rowMin ?? 1;
|
||||
const rowMax = this.rowMax ?? this.rows;
|
||||
const columnMin = this.columnMin ?? 1;
|
||||
const columnMax = this.columnMax ?? this.columns;
|
||||
const columnMin = Math.ceil((this.columnMin ?? 1) / this.step) * this.step;
|
||||
const columnMax =
|
||||
Math.ceil((this.columnMax ?? this.columns) / this.step) * this.step;
|
||||
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
|
||||
const columnValue = this._localValue?.columns;
|
||||
|
||||
@@ -67,9 +68,11 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.max=${columnMax}
|
||||
.range=${this.columns}
|
||||
.value=${fullWidth ? this.columns : this.value?.columns}
|
||||
.step=${this.step}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledColumns}
|
||||
tooltip-mode="always"
|
||||
></ha-grid-layout-slider>
|
||||
|
||||
<ha-grid-layout-slider
|
||||
@@ -85,6 +88,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledRows}
|
||||
tooltip-mode="always"
|
||||
></ha-grid-layout-slider>
|
||||
${!this.isDefault
|
||||
? html`
|
||||
@@ -102,34 +106,44 @@ export class HaGridSizeEditor extends LitElement {
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="preview ${classMap({ "full-width": fullWidth })}"
|
||||
style=${styleMap({
|
||||
"--total-rows": this.rows,
|
||||
"--total-columns": this.columns,
|
||||
"--rows": rowValue,
|
||||
"--columns": fullWidth ? this.columns : columnValue,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
${Array(this.rows * this.columns)
|
||||
<div class="preview">
|
||||
<table>
|
||||
${Array(this.rows)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const row = Math.floor(index / this.columns) + 1;
|
||||
const column = (index % this.columns) + 1;
|
||||
const row = index + 1;
|
||||
return html`
|
||||
<div
|
||||
class="cell"
|
||||
data-row=${row}
|
||||
data-column=${column}
|
||||
@click=${this._cellClick}
|
||||
></div>
|
||||
<tr>
|
||||
${Array(this.columns)
|
||||
.fill(0)
|
||||
.map((__, columnIndex) => {
|
||||
const column = columnIndex + 1;
|
||||
if (
|
||||
column % this.step !== 0 ||
|
||||
(this.columns > 24 && column % 3 !== 0)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<td
|
||||
data-row=${row}
|
||||
data-column=${column}
|
||||
@click=${this._cellClick}
|
||||
></td>
|
||||
`;
|
||||
})}
|
||||
</tr>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="selected">
|
||||
<div class="cell"></div>
|
||||
</div>
|
||||
</table>
|
||||
<div
|
||||
class="preview-card"
|
||||
style=${styleMap({
|
||||
"--rows": rowValue,
|
||||
"--columns": fullWidth ? this.columns : columnValue,
|
||||
"--total-columns": this.columns,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -223,42 +237,40 @@ export class HaGridSizeEditor extends LitElement {
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
}
|
||||
.preview > div {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||
grid-template-rows: repeat(var(--total-rows), 25px);
|
||||
gap: 4px;
|
||||
.preview table,
|
||||
.preview tr,
|
||||
.preview td {
|
||||
border: 2px dotted var(--divider-color);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.preview .cell {
|
||||
background-color: var(--disabled-color);
|
||||
grid-column: span 1;
|
||||
grid-row: span 1;
|
||||
border-radius: 4px;
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.preview .selected {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
.preview table {
|
||||
width: 100%;
|
||||
}
|
||||
.selected .cell {
|
||||
background-color: var(--primary-color);
|
||||
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
|
||||
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
|
||||
opacity: 0.5;
|
||||
.preview tr {
|
||||
height: 30px;
|
||||
}
|
||||
.preview.full-width .selected .cell {
|
||||
grid-column: 1 / -1;
|
||||
.preview td {
|
||||
cursor: pointer;
|
||||
}
|
||||
.preview-card {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.3;
|
||||
border-radius: 8px;
|
||||
height: calc(var(--rows, 1) * 30px);
|
||||
width: calc(var(--columns, 1) * 100% / var(--total-columns, 12));
|
||||
pointer-events: none;
|
||||
transition:
|
||||
width ease-in-out 180ms,
|
||||
height ease-in-out 180ms;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -20,6 +20,8 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@property() public url?: string;
|
||||
|
||||
@property({ attribute: "poster-url" }) public posterUrl?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "controls" })
|
||||
@@ -94,14 +96,19 @@ class HaHLSPlayer extends LitElement {
|
||||
super.updated(changedProps);
|
||||
|
||||
const entityChanged = changedProps.has("entityid");
|
||||
const urlChanged = changedProps.has("url");
|
||||
|
||||
if (!entityChanged) {
|
||||
return;
|
||||
if (entityChanged) {
|
||||
this._getStreamUrlFromEntityId();
|
||||
} else if (urlChanged && this.url) {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
this._url = this.url;
|
||||
this._startHls();
|
||||
}
|
||||
this._getStreamUrl();
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
private async _getStreamUrlFromEntityId(): Promise<void> {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class HaLabel extends LitElement {
|
||||
0.15
|
||||
);
|
||||
--ha-label-background-opacity: 1;
|
||||
border: 1px solid var(--outline-color);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -164,8 +164,8 @@ export class HaMdDialog extends MdDialog {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
:host(:not([type="alert"])) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
@@ -178,7 +178,7 @@ export class HaMdDialog extends MdDialog {
|
||||
}
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dialog-header) {
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export class HaMdDialog extends MdDialog {
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; // overlay navigation
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -117,8 +117,8 @@ export class HaPasswordField extends LitElement {
|
||||
.autocapitalize=${this.autocapitalize}
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
.suffix=${html`<div style="width: 24px"></div>`}
|
||||
@input=${this._handleInputChange}
|
||||
@change=${this._reDispatchEvent}
|
||||
@input=${this._handleInputEvent}
|
||||
@change=${this._handleChangeEvent}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
@@ -153,11 +153,16 @@ export class HaPasswordField extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleInputChange(ev) {
|
||||
private _handleInputEvent(ev) {
|
||||
this.value = ev.target.value;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleChangeEvent(ev) {
|
||||
this.value = ev.target.value;
|
||||
this._reDispatchEvent(ev);
|
||||
}
|
||||
|
||||
private _reDispatchEvent(oldEvent: Event) {
|
||||
const newEvent = new Event(oldEvent.type, oldEvent);
|
||||
this.dispatchEvent(newEvent);
|
||||
|
||||
@@ -135,6 +135,10 @@ export class HaSortable extends LitElement {
|
||||
const Sortable = (await import("../resources/sortable")).default;
|
||||
|
||||
const options: SortableInstance.Options = {
|
||||
scroll: true,
|
||||
// Force the autoscroll fallback because it works better than the native one
|
||||
forceAutoScrollFallback: true,
|
||||
scrollSpeed: 20,
|
||||
animation: 150,
|
||||
...this.options,
|
||||
onChoose: this._handleChoose,
|
||||
|
||||
@@ -130,6 +130,7 @@ export class HaYamlEditor extends LitElement {
|
||||
this._yaml = ev.detail.value;
|
||||
let parsed;
|
||||
let isValid = true;
|
||||
let errorMsg;
|
||||
|
||||
if (this._yaml) {
|
||||
try {
|
||||
@@ -137,6 +138,7 @@ export class HaYamlEditor extends LitElement {
|
||||
} catch (err: any) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
}
|
||||
} else {
|
||||
parsed = {};
|
||||
@@ -145,7 +147,11 @@ export class HaYamlEditor extends LitElement {
|
||||
this.value = parsed;
|
||||
this.isValid = isValid;
|
||||
|
||||
fireEvent(this, "value-changed", { value: parsed, isValid } as any);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: parsed,
|
||||
isValid,
|
||||
errorMsg,
|
||||
} as any);
|
||||
}
|
||||
|
||||
get yaml() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import "../../panels/logbook/ha-logbook-renderer";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { describeCondition } from "../../data/automation_i18n";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -121,6 +122,19 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
const data: ActionTraceStep[] = paths[curPath];
|
||||
|
||||
// Extract details from this.selected.config child properties used to add 'alias' (to headline), describeCondition and 'entity_id' (to result)
|
||||
const nestPath = curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.split("/");
|
||||
let currentDetail = this.selected.config;
|
||||
for (let i = 0; i < nestPath.length; i++) {
|
||||
if (
|
||||
!["undefined", "string"].includes(typeof currentDetail[nestPath[i]])
|
||||
) {
|
||||
currentDetail = currentDetail[nestPath[i]];
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(
|
||||
data.map((trace, idx) => {
|
||||
const { path, timestamp, result, error, changed_variables, ...rest } =
|
||||
@@ -134,7 +148,9 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
return html`
|
||||
${curPath === this.selected.path
|
||||
? ""
|
||||
? currentDetail.alias
|
||||
? html`<h2>${currentDetail.alias}</h2>`
|
||||
: nothing
|
||||
: html`<h2>
|
||||
${curPath.substring(this.selected.path.length + 1)}
|
||||
</h2>`}
|
||||
@@ -146,6 +162,15 @@ export class HaTracePathDetails extends LitElement {
|
||||
{ number: idx + 1 }
|
||||
)}
|
||||
</h3>`}
|
||||
${curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.includes("condition")
|
||||
? html`[${describeCondition(
|
||||
currentDetail,
|
||||
this.hass,
|
||||
currentDetail.alias
|
||||
)}]<br />`
|
||||
: nothing}
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.executed",
|
||||
{
|
||||
@@ -176,6 +201,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
${Object.keys(rest).length === 0
|
||||
? nothing
|
||||
: html`<pre>${dump(rest)}</pre>`}
|
||||
${currentDetail.entity_id &&
|
||||
curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.includes("entity_id")
|
||||
? html`<pre>entity: ${currentDetail.entity_id}</pre>`
|
||||
: nothing}
|
||||
`;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -30,11 +30,11 @@ export const autocompleteLoginFields = (schema: HaFormSchema[]) =>
|
||||
if (field.type !== "string") return field;
|
||||
switch (field.name) {
|
||||
case "username":
|
||||
return { ...field, autocomplete: "username" };
|
||||
return { ...field, autocomplete: "username", autofocus: true };
|
||||
case "password":
|
||||
return { ...field, autocomplete: "current-password" };
|
||||
case "code":
|
||||
return { ...field, autocomplete: "one-time-code" };
|
||||
return { ...field, autocomplete: "one-time-code", autofocus: true };
|
||||
default:
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { formatDuration } from "../common/datetime/format_duration";
|
||||
import {
|
||||
formatDuration,
|
||||
formatDurationLong,
|
||||
} from "../common/datetime/format_duration";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
@@ -720,6 +723,38 @@ const tryDescribeTrigger = (
|
||||
}`;
|
||||
}
|
||||
|
||||
// Calendar Trigger
|
||||
if (trigger.trigger === "calendar") {
|
||||
const calendarEntity = hass.states[trigger.entity_id]
|
||||
? computeStateName(hass.states[trigger.entity_id])
|
||||
: trigger.entity_id;
|
||||
|
||||
let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||
let offset: string | string[] = trigger.offset.startsWith("-")
|
||||
? trigger.offset.substring(1).split(":")
|
||||
: trigger.offset.split(":");
|
||||
const duration = {
|
||||
hours: offset.length > 0 ? +offset[0] : 0,
|
||||
minutes: offset.length > 1 ? +offset[1] : 0,
|
||||
seconds: offset.length > 2 ? +offset[2] : 0,
|
||||
};
|
||||
offset = formatDurationLong(hass.locale, duration);
|
||||
if (offset === "") {
|
||||
offsetChoice = "other";
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.calendar.description.full`,
|
||||
{
|
||||
eventChoice: trigger.event,
|
||||
offsetChoice: offsetChoice,
|
||||
offset: offset,
|
||||
hasCalendar: trigger.entity_id ? "true" : "false",
|
||||
calendar: calendarEntity,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
|
||||
|
||||
@@ -1,36 +1,98 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface BackupAgent {
|
||||
agent_id: string;
|
||||
}
|
||||
|
||||
export interface BackupContent {
|
||||
slug: string;
|
||||
backup_id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
protected: boolean;
|
||||
size: number;
|
||||
path: string;
|
||||
agent_ids?: string[];
|
||||
}
|
||||
|
||||
export interface BackupData {
|
||||
backing_up: boolean;
|
||||
export interface BackupInfo {
|
||||
backups: BackupContent[];
|
||||
backing_up: boolean;
|
||||
}
|
||||
|
||||
export const getBackupDownloadUrl = (slug: string) =>
|
||||
`/api/backup/download/${slug}`;
|
||||
export interface BackupDetails {
|
||||
backup: BackupContent;
|
||||
}
|
||||
|
||||
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
|
||||
export interface BackupAgentsInfo {
|
||||
agents: BackupAgent[];
|
||||
}
|
||||
|
||||
export type GenerateBackupParams = {
|
||||
agent_ids: string[];
|
||||
database_included?: boolean;
|
||||
folders_included?: string[];
|
||||
addons_included?: string[];
|
||||
name?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export const getBackupDownloadUrl = (id: string, agentId: string) =>
|
||||
`/api/backup/download/${id}?agent_id=${agentId}`;
|
||||
|
||||
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
|
||||
hass.callWS({
|
||||
type: "backup/info",
|
||||
});
|
||||
|
||||
export const removeBackup = (
|
||||
export const fetchBackupDetails = (
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
): Promise<void> =>
|
||||
id: string
|
||||
): Promise<BackupDetails> =>
|
||||
hass.callWS({
|
||||
type: "backup/remove",
|
||||
slug,
|
||||
type: "backup/details",
|
||||
backup_id: id,
|
||||
});
|
||||
|
||||
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
|
||||
export const fetchBackupAgentsInfo = (
|
||||
hass: HomeAssistant
|
||||
): Promise<BackupAgentsInfo> =>
|
||||
hass.callWS({
|
||||
type: "backup/agents/info",
|
||||
});
|
||||
|
||||
export const removeBackup = (hass: HomeAssistant, id: string): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "backup/remove",
|
||||
backup_id: id,
|
||||
});
|
||||
|
||||
export const generateBackup = (
|
||||
hass: HomeAssistant,
|
||||
params: GenerateBackupParams
|
||||
): Promise<{ backup_id: string }> =>
|
||||
hass.callWS({
|
||||
type: "backup/generate",
|
||||
...params,
|
||||
});
|
||||
|
||||
export const uploadBackup = async (
|
||||
hass: HomeAssistant,
|
||||
file: File
|
||||
): Promise<void> => {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const resp = await hass.fetchWithAuth("/api/backup/upload", {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPreferredAgentForDownload = (agents: string[]) => {
|
||||
const localAgents = agents.filter(
|
||||
(agent) => agent.split(".")[0] === "backup"
|
||||
);
|
||||
return localAgents[0] || agents[0];
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ export const STATE_ATTRIBUTES = [
|
||||
"state_class",
|
||||
"supported_features",
|
||||
"unit_of_measurement",
|
||||
"available_tones",
|
||||
];
|
||||
|
||||
export const TEMPERATURE_ATTRIBUTES = new Set([
|
||||
|
||||
@@ -10,8 +10,6 @@ import type { LightColor } from "./light";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeEntityRegistryDisplay } from "./ws-entity_registry_display";
|
||||
|
||||
type EntityCategory = "config" | "diagnostic";
|
||||
|
||||
export interface EntityRegistryDisplayEntry {
|
||||
|
||||
@@ -185,6 +185,15 @@ export const fetchHassioInfo = async (
|
||||
export const fetchHassioBoots = async (hass: HomeAssistant) =>
|
||||
hass.callApi<HassioResponse<HassioBoots>>("GET", `hassio/host/logs/boots`);
|
||||
|
||||
export const fetchHassioLogsLegacy = async (
|
||||
hass: HomeAssistant,
|
||||
provider: string
|
||||
) =>
|
||||
hass.callApi<string>(
|
||||
"GET",
|
||||
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
|
||||
);
|
||||
|
||||
export const fetchHassioLogs = async (
|
||||
hass: HomeAssistant,
|
||||
provider: string,
|
||||
|
||||
@@ -63,8 +63,8 @@ const triggerPhrases: Record<TriggerPhraseKeys, string> = {
|
||||
triggered_by_numeric_state_of: "numeric state of", // number state trigger
|
||||
triggered_by_state_of: "state of", // state trigger
|
||||
triggered_by_event: "event", // event trigger
|
||||
triggered_by_time: "time", // time trigger
|
||||
triggered_by_time_pattern: "time pattern", // time trigger
|
||||
triggered_by_time: "time", // time trigger
|
||||
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
|
||||
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
|
||||
};
|
||||
@@ -218,114 +218,32 @@ export const localizeStateMessage = (
|
||||
const isOff = state === BINARY_STATE_OFF;
|
||||
const device_class = stateObj.attributes.device_class;
|
||||
|
||||
switch (device_class) {
|
||||
case "battery":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_low`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_normal`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "connectivity":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_connected`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_disconnected`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "door":
|
||||
case "garage_door":
|
||||
case "opening":
|
||||
case "window":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "lock":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "plug":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_plugged_in`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unplugged`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "presence":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "safety":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unsafe`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_safe`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "cold":
|
||||
case "gas":
|
||||
case "heat":
|
||||
case "moisture":
|
||||
case "motion":
|
||||
case "occupancy":
|
||||
case "power":
|
||||
case "problem":
|
||||
case "smoke":
|
||||
case "sound":
|
||||
case "vibration":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_device_class`, {
|
||||
if (device_class && (isOn || isOff)) {
|
||||
return (
|
||||
localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_classes" : "cleared_device_classes"}.${device_class}`,
|
||||
{
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
),
|
||||
) || device_class,
|
||||
hass.language
|
||||
),
|
||||
});
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_device_class`, {
|
||||
}
|
||||
) ||
|
||||
// If there's no key for a specific device class, fallback to generic string
|
||||
localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_class" : "cleared_device_class"}`,
|
||||
{
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
),
|
||||
) || device_class,
|
||||
hass.language
|
||||
),
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "tamper":
|
||||
if (isOn) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_tampering`);
|
||||
}
|
||||
if (isOff) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.cleared_tampering`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
HassEntity,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -871,3 +874,65 @@ export const computeCreateDomains = (
|
||||
|
||||
return [...new Set(createDomains)];
|
||||
};
|
||||
|
||||
export const resolveEntityIDs = (
|
||||
hass: HomeAssistant,
|
||||
targetPickerValue: HassServiceTarget,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"]
|
||||
): string[] => {
|
||||
if (!targetPickerValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetSelector = { target: {} };
|
||||
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
|
||||
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
|
||||
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
|
||||
const targetFloors = new Set(ensureArray(targetPickerValue.floor_id));
|
||||
const targetLabels = new Set(ensureArray(targetPickerValue.label_id));
|
||||
|
||||
targetLabels.forEach((labelId) => {
|
||||
const expanded = expandLabelTarget(
|
||||
hass,
|
||||
labelId,
|
||||
areas,
|
||||
devices,
|
||||
entities,
|
||||
targetSelector
|
||||
);
|
||||
expanded.devices.forEach((id) => targetDevices.add(id));
|
||||
expanded.entities.forEach((id) => targetEntities.add(id));
|
||||
expanded.areas.forEach((id) => targetAreas.add(id));
|
||||
});
|
||||
|
||||
targetFloors.forEach((floorId) => {
|
||||
const expanded = expandFloorTarget(hass, floorId, areas, targetSelector);
|
||||
expanded.areas.forEach((id) => targetAreas.add(id));
|
||||
});
|
||||
|
||||
targetAreas.forEach((areaId) => {
|
||||
const expanded = expandAreaTarget(
|
||||
hass,
|
||||
areaId,
|
||||
devices,
|
||||
entities,
|
||||
targetSelector
|
||||
);
|
||||
expanded.devices.forEach((id) => targetDevices.add(id));
|
||||
expanded.entities.forEach((id) => targetEntities.add(id));
|
||||
});
|
||||
|
||||
targetDevices.forEach((deviceId) => {
|
||||
const expanded = expandDeviceTarget(
|
||||
hass,
|
||||
deviceId,
|
||||
entities,
|
||||
targetSelector
|
||||
);
|
||||
expanded.entities.forEach((id) => targetEntities.add(id));
|
||||
});
|
||||
|
||||
return Array.from(targetEntities);
|
||||
};
|
||||
|
||||
7
src/data/siren.ts
Normal file
7
src/data/siren.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const SirenEntityFeature = {
|
||||
TURN_ON: 1,
|
||||
TURN_OFF: 2,
|
||||
TONES: 4,
|
||||
VOLUME_SET: 8,
|
||||
DURATION: 16,
|
||||
};
|
||||
@@ -209,6 +209,17 @@ export interface ZWaveJSNodeStatus {
|
||||
has_firmware_update_cc: boolean;
|
||||
}
|
||||
|
||||
export type ZWaveJSNodeCapabilities = {
|
||||
[endpoint: number]: ZWaveJSEndpointCapability[];
|
||||
};
|
||||
|
||||
export interface ZWaveJSEndpointCapability {
|
||||
id: number;
|
||||
name: string;
|
||||
version: number;
|
||||
is_secure: boolean;
|
||||
}
|
||||
|
||||
export interface ZwaveJSNodeMetadata {
|
||||
node_id: number;
|
||||
exclusion: string;
|
||||
@@ -264,6 +275,15 @@ export interface ZWaveJSSetConfigParamData {
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSSetRawConfigParamData {
|
||||
type: string;
|
||||
device_id: string;
|
||||
property: number;
|
||||
value: number;
|
||||
value_size: number;
|
||||
value_format: number;
|
||||
}
|
||||
|
||||
export interface ZWaveJSSetConfigParamResult {
|
||||
value_id?: string;
|
||||
status?: string;
|
||||
@@ -404,6 +424,25 @@ export interface RequestedGrant {
|
||||
clientSideAuth: boolean;
|
||||
}
|
||||
|
||||
export const invokeZWaveCCApi = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
command_class: number,
|
||||
endpoint: number | undefined,
|
||||
method_name: string,
|
||||
parameters: any[],
|
||||
wait_for_result?: boolean
|
||||
): Promise<unknown> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/invoke_cc_api",
|
||||
device_id,
|
||||
command_class,
|
||||
endpoint,
|
||||
method_name,
|
||||
parameters,
|
||||
wait_for_result,
|
||||
});
|
||||
|
||||
export const fetchZwaveNetworkStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_or_entry_id: {
|
||||
@@ -579,6 +618,15 @@ export const fetchZwaveNodeStatus = (
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const fetchZwaveNodeCapabilities = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string
|
||||
): Promise<ZWaveJSNodeCapabilities> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/node_capabilities",
|
||||
device_id,
|
||||
});
|
||||
|
||||
export const subscribeZwaveNodeStatus = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
@@ -638,6 +686,36 @@ export const setZwaveNodeConfigParameter = (
|
||||
return hass.callWS(data);
|
||||
};
|
||||
|
||||
export const setZwaveNodeRawConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
property: number,
|
||||
value: number,
|
||||
value_size: number,
|
||||
value_format: number
|
||||
): Promise<ZWaveJSSetConfigParamResult> => {
|
||||
const data: ZWaveJSSetRawConfigParamData = {
|
||||
type: "zwave_js/set_raw_config_parameter",
|
||||
device_id,
|
||||
property,
|
||||
value,
|
||||
value_size,
|
||||
value_format,
|
||||
};
|
||||
return hass.callWS(data);
|
||||
};
|
||||
|
||||
export const getZwaveNodeRawConfigParameter = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
property: number
|
||||
): Promise<number> =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/get_raw_config_parameter",
|
||||
device_id,
|
||||
property,
|
||||
});
|
||||
|
||||
export const reinterviewZwaveNode = (
|
||||
hass: HomeAssistant,
|
||||
device_id: string,
|
||||
|
||||
136
src/dialogs/backup/dialog-backup-upload.ts
Normal file
136
src/dialogs/backup/dialog-backup-upload.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { mdiClose, mdiFolderUpload } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-file-upload";
|
||||
import "../../components/ha-header-bar";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-icon-button";
|
||||
import { uploadBackup } from "../../data/backup";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { BackupUploadDialogParams } from "./show-dialog-backup-upload";
|
||||
|
||||
const SUPPORTED_FORMAT = "application/x-tar";
|
||||
|
||||
@customElement("dialog-backup-upload")
|
||||
export class DialogBackupUpload
|
||||
extends LitElement
|
||||
implements HassDialog<BackupUploadDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _dialogParams?: BackupUploadDialogParams;
|
||||
|
||||
@state() private _uploading = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: BackupUploadDialogParams
|
||||
): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialogParams = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._dialogParams || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
heading="Upload backup"
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title"> Upload backup </span>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
slot="actionItems"
|
||||
dialogAction="cancel"
|
||||
dialogInitialFocus
|
||||
></ha-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept=${SUPPORTED_FORMAT}
|
||||
label="Upload a backup"
|
||||
supports="Supports .tar files"
|
||||
@file-picked=${this._uploadFile}
|
||||
></ha-file-upload>
|
||||
${this._error
|
||||
? html`<ha-alert alertType="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _uploadFile(ev: CustomEvent<{ files: File[] }>): Promise<void> {
|
||||
this._error = undefined;
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (file.type !== SUPPORTED_FORMAT) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
text: "Please choose a Home Assistant backup file (.tar)",
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
try {
|
||||
await uploadBackup(this.hass!, file);
|
||||
this._dialogParams!.onUploadComplete();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message;
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-backup-upload": DialogBackupUpload;
|
||||
}
|
||||
}
|
||||
17
src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
17
src/dialogs/backup/show-dialog-backup-upload.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "./dialog-backup-upload";
|
||||
|
||||
export interface BackupUploadDialogParams {
|
||||
onUploadComplete: () => void;
|
||||
}
|
||||
|
||||
export const showBackupUploadDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: BackupUploadDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-backup-upload",
|
||||
dialogImport: () => import("./dialog-backup-upload"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { SirenEntityFeature } from "../../../../data/siren";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
|
||||
@customElement("ha-more-info-siren-advanced-controls")
|
||||
class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() _stateObj?: HassEntity;
|
||||
|
||||
@state() _tone?: string;
|
||||
|
||||
@state() _volume?: number;
|
||||
|
||||
@state() _duration?: number;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog({ stateObj }: { stateObj: HassEntity }) {
|
||||
this._stateObj = stateObj;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._stateObj = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this._stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const supportsTones =
|
||||
supportsFeature(this._stateObj, SirenEntityFeature.TONES) &&
|
||||
this._stateObj.attributes.available_tones;
|
||||
const supportsVolume = supportsFeature(
|
||||
this._stateObj,
|
||||
SirenEntityFeature.VOLUME_SET
|
||||
);
|
||||
const supportsDuration = supportsFeature(
|
||||
this._stateObj,
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="options">
|
||||
${supportsTones
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.components.siren.tone")}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleToneChange}
|
||||
.value=${this._tone}
|
||||
>
|
||||
${Object.entries(
|
||||
this._stateObj.attributes.available_tones
|
||||
).map(
|
||||
([toneId, toneName]) => html`
|
||||
<ha-list-item .value=${toneId}
|
||||
>${toneName}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
${supportsVolume
|
||||
? html`
|
||||
<ha-textfield
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.volume")}
|
||||
.suffix=${"%"}
|
||||
.value=${this._volume ? this._volume * 100 : undefined}
|
||||
@change=${this._handleVolumeChange}
|
||||
.min=${0}
|
||||
.max=${100}
|
||||
.step=${1}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
${supportsDuration
|
||||
? html`
|
||||
<ha-textfield
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.duration")}
|
||||
.value=${this._duration}
|
||||
suffix="s"
|
||||
@change=${this._handleDurationChange}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.common.turn_on")}
|
||||
@click=${this._turnOn}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.common.turn_off")}
|
||||
@click=${this._turnOff}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev) {
|
||||
this._tone = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleVolumeChange(ev) {
|
||||
this._volume = parseFloat(ev.target.value) / 100;
|
||||
if (isNaN(this._volume)) {
|
||||
this._volume = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDurationChange(ev) {
|
||||
this._duration = parseInt(ev.target.value);
|
||||
if (isNaN(this._duration)) {
|
||||
this._duration = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _turnOn() {
|
||||
await this.hass.callService("siren", "turn_on", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
tone: this._tone,
|
||||
volume: this._volume,
|
||||
duration: this._duration,
|
||||
});
|
||||
}
|
||||
|
||||
private async _turnOff() {
|
||||
await this.hass.callService("siren", "turn_off", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-border-radius: 16px;
|
||||
--mdc-icon-size: 24px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-siren-advanced-controls": MoreInfoSirenAdvancedControls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export const loadSirenAdvancedControlsView = () =>
|
||||
import("./ha-more-info-siren-advanced-controls");
|
||||
|
||||
export const showSirenAdvancedControlsView = (
|
||||
element: HTMLElement,
|
||||
stateObj: HassEntity
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-more-info-siren-advanced-controls",
|
||||
dialogImport: loadSirenAdvancedControlsView,
|
||||
dialogParams: {
|
||||
stateObj,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,14 +4,20 @@ import { property, state } from "lit/decorators";
|
||||
import "../../../components/ha-camera-stream";
|
||||
import type { CameraEntity } from "../../../data/camera";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showToast } from "../../../util/toast";
|
||||
|
||||
class MoreInfoCamera extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@state() private _attached = false;
|
||||
|
||||
@state() private _waiting = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._attached = true;
|
||||
@@ -23,7 +29,7 @@ class MoreInfoCamera extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._attached || !this.hass || !this.stateObj) {
|
||||
if (!this._attached || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -34,14 +40,70 @@ class MoreInfoCamera extends LitElement {
|
||||
allow-exoplayer
|
||||
controls
|
||||
></ha-camera-stream>
|
||||
|
||||
<div class="actions">
|
||||
<ha-progress-button
|
||||
@click=${this._downloadSnapshot}
|
||||
.progress=${this._waiting}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.camera.download_snapshot"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _downloadSnapshot(ev: CustomEvent) {
|
||||
const button = ev.currentTarget as any;
|
||||
this._waiting = true;
|
||||
|
||||
try {
|
||||
const result: Response | undefined = await this.hass.callApiRaw(
|
||||
"GET",
|
||||
`camera_proxy/${this.stateObj!.entity_id}`
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
throw new Error("No response from API");
|
||||
}
|
||||
|
||||
const blob = await result.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url);
|
||||
} catch (err) {
|
||||
this._waiting = false;
|
||||
button.actionError();
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.camera.failed_to_download"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._waiting = false;
|
||||
button.actionSuccess();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
z-index: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../state-control/ha-state-control-toggle";
|
||||
import "../../../components/ha-button";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { SirenEntityFeature } from "../../../data/siren";
|
||||
import { showSirenAdvancedControlsView } from "../components/siren/show-dialog-siren-advanced-controls";
|
||||
|
||||
@customElement("more-info-siren")
|
||||
class MoreInfoSiren extends LitElement {
|
||||
@@ -20,6 +24,20 @@ class MoreInfoSiren extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsTones =
|
||||
supportsFeature(this.stateObj, SirenEntityFeature.TONES) &&
|
||||
this.stateObj.attributes.available_tones;
|
||||
const supportsVolume = supportsFeature(
|
||||
this.stateObj,
|
||||
SirenEntityFeature.VOLUME_SET
|
||||
);
|
||||
const supportsDuration = supportsFeature(
|
||||
this.stateObj,
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
// show advanced controls dialog if extra features are supported
|
||||
const allowAdvanced = supportsTones || supportsVolume || supportsDuration;
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
@@ -32,6 +50,11 @@ class MoreInfoSiren extends LitElement {
|
||||
.iconPathOn=${mdiVolumeHigh}
|
||||
.iconPathOff=${mdiVolumeOff}
|
||||
></ha-state-control-toggle>
|
||||
${allowAdvanced
|
||||
? html`<ha-button @click=${this._showAdvancedControlsDialog}>
|
||||
${this.hass.localize("ui.components.siren.advanced_controls")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
@@ -40,6 +63,10 @@ class MoreInfoSiren extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _showAdvancedControlsDialog() {
|
||||
showSirenAdvancedControlsView(this, this.stateObj!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return moreInfoControlStyle;
|
||||
}
|
||||
|
||||
@@ -9,16 +9,8 @@
|
||||
<script>
|
||||
(function() {
|
||||
if (!window.latestJS) {
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
System.import("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
<% } else { %>
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>", true);
|
||||
<% } %>
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>", true);
|
||||
<% } %>
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -97,16 +97,8 @@
|
||||
<script>
|
||||
if (!window.latestJS) {
|
||||
window.customPanelJS = "<%= es5CustomPanelJS %>";
|
||||
<% if (useRollup) { %>
|
||||
_ls("/static/js/s.min.js").onload = function() {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
System.import("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
<% } else { %>
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>", true);
|
||||
<% } %>
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
_ls("<%= entry %>", true);
|
||||
<% } %>
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -456,6 +456,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="top_header"></slot>
|
||||
<slot name="header">
|
||||
<div class="table-header">
|
||||
${this.hasFilters && !this.showFilters
|
||||
|
||||
@@ -173,7 +173,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
.label=${this.hass.localize("ui.components.calendar.event.summary")}
|
||||
.value=${this._summary}
|
||||
required
|
||||
@change=${this._handleSummaryChanged}
|
||||
@input=${this._handleSummaryChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
@@ -103,6 +103,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _mode: "gui" | "yaml" = "gui";
|
||||
@@ -629,15 +631,17 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = {
|
||||
id: this._config?.id,
|
||||
...normalizeAutomationConfig(ev.detail.value),
|
||||
};
|
||||
this._errors = undefined;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private async confirmUnsavedChanged(): Promise<boolean> {
|
||||
@@ -752,7 +756,21 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _switchUiMode() {
|
||||
private async _switchUiMode() {
|
||||
if (this._yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this._yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._mode = "gui";
|
||||
}
|
||||
|
||||
@@ -792,6 +810,13 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _saveAutomation(): Promise<void> {
|
||||
if (this._yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.automationId || String(Date.now());
|
||||
if (!this.automationId) {
|
||||
const saved = await this._promptAutomationAlias();
|
||||
|
||||
104
src/panels/config/backup/components/ha-backup-agents-select.ts
Normal file
104
src/panels/config/backup/components/ha-backup-agents-select.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-formfield";
|
||||
import type { BackupAgent } from "../../../../data/backup";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { brandsUrl } from "../../../../util/brands-url";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
|
||||
@customElement("ha-backup-agents-select")
|
||||
class HaBackupAgentsSelect extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public agents!: BackupAgent[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public disabledAgents?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public value!: string[];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="agents">
|
||||
${this.agents.map((agent) => this._renderAgent(agent))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAgent(agent: BackupAgent) {
|
||||
const [domain, name] = agent.agent_id.split(".");
|
||||
const domainName = domainToName(this.hass.localize, domain);
|
||||
return html`
|
||||
<ha-formfield>
|
||||
<span class="label" slot="label">
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
slot="start"
|
||||
/>
|
||||
${domainName}: ${name}</span
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.value.includes(agent.agent_id)}
|
||||
.value=${agent.agent_id}
|
||||
.disabled=${this.disabled ||
|
||||
this.disabledAgents?.includes(agent.agent_id)}
|
||||
@change=${this._checkboxChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev: Event) {
|
||||
const checkbox = ev.target as HTMLInputElement;
|
||||
const value = checkbox.value;
|
||||
const index = this.value.indexOf(value);
|
||||
if (checkbox.checked && index === -1) {
|
||||
this.value = [...this.value, value];
|
||||
} else if (!checkbox.checked && index !== -1) {
|
||||
this.value = [
|
||||
...this.value.slice(0, index),
|
||||
...this.value.slice(index + 1),
|
||||
];
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
.agents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-backup-agents-select": HaBackupAgentsSelect;
|
||||
}
|
||||
}
|
||||
149
src/panels/config/backup/components/ha-backup-summary-card.ts
Normal file
149
src/panels/config/backup/components/ha-backup-summary-card.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
mdiAlertCircleCheckOutline,
|
||||
mdiAlertOutline,
|
||||
mdiCheck,
|
||||
mdiInformationOutline,
|
||||
mdiSync,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/ha-icon";
|
||||
|
||||
type SummaryStatus = "success" | "error" | "info" | "warning" | "loading";
|
||||
|
||||
const ICONS: Record<SummaryStatus, string> = {
|
||||
success: mdiCheck,
|
||||
error: mdiAlertCircleCheckOutline,
|
||||
warning: mdiAlertOutline,
|
||||
info: mdiInformationOutline,
|
||||
loading: mdiSync,
|
||||
};
|
||||
|
||||
@customElement("ha-backup-summary-card")
|
||||
class HaBackupSummaryCard extends LitElement {
|
||||
@property()
|
||||
public title!: string;
|
||||
|
||||
@property()
|
||||
public description!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-action" })
|
||||
public hasAction = false;
|
||||
|
||||
@property()
|
||||
public status: SummaryStatus = "info";
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<div class="summary">
|
||||
${this.status === "loading"
|
||||
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||
: html`
|
||||
<div class="icon ${this.status}">
|
||||
<ha-svg-icon .path=${ICONS[this.status]}></ha-svg-icon>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<div class="content">
|
||||
<p class="title">${this.title}</p>
|
||||
<p class="description">${this.description}</p>
|
||||
</div>
|
||||
${this.hasAction
|
||||
? html`
|
||||
<div class="action">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.icon {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
--icon-color: var(--primary-color);
|
||||
}
|
||||
.icon.success {
|
||||
--icon-color: var(--success-color);
|
||||
}
|
||||
.icon.warning {
|
||||
--icon-color: var(--warning-color);
|
||||
}
|
||||
.icon.error {
|
||||
--icon-color: var(--error-color);
|
||||
}
|
||||
.icon::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--icon-color, var(--primary-color));
|
||||
opacity: 0.2;
|
||||
}
|
||||
.icon ha-svg-icon {
|
||||
color: var(--icon-color, var(--primary-color));
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
--md-circular-progress-size: 40px;
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
color: var(--primary-text-color);
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.description {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
color: var(--secondary-text-color);
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-backup-summary-card": HaBackupSummaryCard;
|
||||
}
|
||||
}
|
||||
375
src/panels/config/backup/dialogs/dialog-generate-backup.ts
Normal file
375
src/panels/config/backup/dialogs/dialog-generate-backup.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import {
|
||||
mdiChartBox,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFolder,
|
||||
mdiPlayBoxMultiple,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-select";
|
||||
import "../../../../components/ha-md-select-option";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-textfield";
|
||||
import type { BackupAgent } from "../../../../data/backup";
|
||||
import { fetchBackupAgentsInfo, generateBackup } from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../components/ha-backup-agents-select";
|
||||
import type { GenerateBackupDialogParams } from "./show-dialog-generate-backup";
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
history: boolean;
|
||||
media: boolean;
|
||||
share: boolean;
|
||||
addons_mode: "all" | "custom";
|
||||
addons: string[];
|
||||
agents_mode: "all" | "custom";
|
||||
agents: string[];
|
||||
};
|
||||
|
||||
const INITIAL_FORM_DATA: FormData = {
|
||||
name: "",
|
||||
history: true,
|
||||
media: false,
|
||||
share: false,
|
||||
addons_mode: "all",
|
||||
addons: [],
|
||||
agents_mode: "all",
|
||||
agents: [],
|
||||
};
|
||||
|
||||
const STEPS = ["data", "sync"] as const;
|
||||
|
||||
@customElement("ha-dialog-generate-backup")
|
||||
class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _formData?: FormData;
|
||||
|
||||
@state() private _step?: "data" | "sync";
|
||||
|
||||
@state() private _agents: BackupAgent[] = [];
|
||||
|
||||
@state() private _params?: GenerateBackupDialogParams;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog(_params: GenerateBackupDialogParams): void {
|
||||
this._step = STEPS[0];
|
||||
this._formData = INITIAL_FORM_DATA;
|
||||
this._params = _params;
|
||||
this._fetchAgents();
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (this._params!.cancel) {
|
||||
this._params!.cancel();
|
||||
}
|
||||
this._step = undefined;
|
||||
this._formData = undefined;
|
||||
this._agents = [];
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private async _fetchAgents() {
|
||||
const { agents } = await fetchBackupAgentsInfo(this.hass);
|
||||
this._agents = agents;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _previousStep() {
|
||||
const index = STEPS.indexOf(this._step!);
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
this._step = STEPS[index - 1];
|
||||
}
|
||||
|
||||
private _nextStep() {
|
||||
const index = STEPS.indexOf(this._step!);
|
||||
if (index === STEPS.length - 1) {
|
||||
return;
|
||||
}
|
||||
this._step = STEPS[index + 1];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._step || !this._formData) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const dialogTitle =
|
||||
this._step === "sync" ? "Synchronization" : "Backup data";
|
||||
|
||||
const isFirstStep = this._step === STEPS[0];
|
||||
const isLastStep = this._step === STEPS[STEPS.length - 1];
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open disable-cancel-action @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
${isFirstStep
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-prev
|
||||
slot="navigationIcon"
|
||||
@click=${this._previousStep}
|
||||
></ha-icon-button-prev>
|
||||
`}
|
||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">
|
||||
${this._step === "data" ? this._renderData() : this._renderSync()}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
${isFirstStep
|
||||
? html`<ha-button @click=${this.closeDialog}>Cancel</ha-button>`
|
||||
: nothing}
|
||||
${isLastStep
|
||||
? html`<ha-button @click=${this._submit}>Create backup</ha-button>`
|
||||
: html`<ha-button @click=${this._nextStep}>Next</ha-button>`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderData() {
|
||||
if (!this._formData) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-settings-row>
|
||||
<ha-svg-icon slot="prefix" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="heading">Home Assistant settings</span>
|
||||
<span slot="description">
|
||||
With these settings you are able to restore your system.
|
||||
</span>
|
||||
<ha-switch disabled checked></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<ha-svg-icon slot="prefix" .path=${mdiChartBox}></ha-svg-icon>
|
||||
<span slot="heading">History</span>
|
||||
<span slot="description">For example of your energy dashboard.</span>
|
||||
<ha-switch
|
||||
id="history"
|
||||
name="history"
|
||||
@change=${this._switchChanged}
|
||||
.checked=${this._formData.history}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<ha-svg-icon slot="prefix" .path=${mdiPlayBoxMultiple}></ha-svg-icon>
|
||||
<span slot="heading">Media</span>
|
||||
<span slot="description">
|
||||
Folder that is often used for advanced or older configurations.
|
||||
</span>
|
||||
<ha-switch
|
||||
id="media"
|
||||
name="media"
|
||||
@change=${this._switchChanged}
|
||||
.checked=${this._formData.media}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<ha-svg-icon slot="prefix" .path=${mdiFolder}></ha-svg-icon>
|
||||
<span slot="heading">Share folder</span>
|
||||
<span slot="description">
|
||||
Folder that is often used for advanced or older configurations.
|
||||
</span>
|
||||
<ha-switch
|
||||
id="share"
|
||||
name="share"
|
||||
@change=${this._switchChanged}
|
||||
.checked=${this._formData.share}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSync() {
|
||||
if (!this._formData) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-textfield
|
||||
name="name"
|
||||
.label=${"Backup name"}
|
||||
.value=${this._formData.name}
|
||||
@change=${this._nameChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Locations</span>
|
||||
<span slot="description">
|
||||
What locations you want to automatically backup to.
|
||||
</span>
|
||||
<ha-md-select
|
||||
@change=${this._agentModeChanged}
|
||||
.value=${this._formData.agents_mode}
|
||||
>
|
||||
<ha-md-select-option value="all">
|
||||
<div slot="headline">All (${this._agents.length})</div>
|
||||
</ha-md-select-option>
|
||||
<ha-md-select-option value="custom">
|
||||
<div slot="headline">Custom</div>
|
||||
</ha-md-select-option>
|
||||
</ha-md-select>
|
||||
</ha-settings-row>
|
||||
${this._formData.agents_mode === "custom"
|
||||
? html`
|
||||
<ha-expansion-panel .header=${"Location"} outlined expanded>
|
||||
<ha-backup-agents-select
|
||||
.hass=${this.hass}
|
||||
.value=${this._formData.agents}
|
||||
@value-changed=${this._agentsChanged}
|
||||
.agents=${this._agents}
|
||||
></ha-backup-agents-select>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _agentModeChanged(ev) {
|
||||
const select = ev.currentTarget;
|
||||
this._formData = {
|
||||
...this._formData!,
|
||||
agents_mode: select.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _agentsChanged(ev) {
|
||||
this._formData = {
|
||||
...this._formData!,
|
||||
agents: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
private _switchChanged(ev) {
|
||||
const _switch = ev.currentTarget;
|
||||
this._formData = {
|
||||
...this._formData!,
|
||||
[_switch.id]: _switch.checked,
|
||||
};
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
this._formData = {
|
||||
...this._formData!,
|
||||
name: ev.target.value,
|
||||
};
|
||||
}
|
||||
|
||||
private async _submit() {
|
||||
if (!this._formData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
addons,
|
||||
addons_mode,
|
||||
agents,
|
||||
agents_mode,
|
||||
history,
|
||||
media,
|
||||
name,
|
||||
share,
|
||||
} = this._formData;
|
||||
|
||||
const folders: string[] = [];
|
||||
if (media) {
|
||||
folders.push("media");
|
||||
}
|
||||
if (share) {
|
||||
folders.push("share");
|
||||
}
|
||||
|
||||
// TODO: Fetch all addons
|
||||
const ALL_ADDONS = [];
|
||||
const { backup_id } = await generateBackup(this.hass, {
|
||||
name,
|
||||
agent_ids:
|
||||
agents_mode === "all"
|
||||
? this._agents.map((agent) => agent.agent_id)
|
||||
: agents,
|
||||
database_included: history,
|
||||
folders_included: folders,
|
||||
addons_included: addons_mode === "all" ? ALL_ADDONS : addons,
|
||||
});
|
||||
|
||||
this._params!.submit?.({ backup_id });
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--dialog-content-overflow: visible;
|
||||
}
|
||||
ha-md-dialog {
|
||||
--dialog-content-padding: 24px;
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-prefix-display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
ha-settings-row > ha-svg-icon {
|
||||
align-self: center;
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
ha-settings-row > ha-md-select {
|
||||
min-width: 150px;
|
||||
}
|
||||
ha-settings-row > ha-md-select > span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-settings-row > ha-md-select-option {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.content {
|
||||
padding-top: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-generate-backup": DialogGenerateBackup;
|
||||
}
|
||||
}
|
||||
147
src/panels/config/backup/dialogs/dialog-new-backup.ts
Normal file
147
src/panels/config/backup/dialogs/dialog-new-backup.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { mdiBackupRestore, mdiClose, mdiCogs } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { NewBackupDialogParams } from "./show-dialog-new-backup";
|
||||
|
||||
@customElement("ha-dialog-new-backup")
|
||||
class DialogNewBackup extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _params?: NewBackupDialogParams;
|
||||
|
||||
public showDialog(params: NewBackupDialogParams): void {
|
||||
this._opened = true;
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (this._params!.cancel) {
|
||||
this._params!.cancel();
|
||||
}
|
||||
if (this._opened) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._opened = false;
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened || !this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const heading = "New backup";
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
aria-labelledby="dialog-box-title"
|
||||
aria-describedby="dialog-box-description"
|
||||
>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title">
|
||||
${heading}
|
||||
</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<ha-md-list
|
||||
innerRole="listbox"
|
||||
itemRoles="option"
|
||||
innerAriaLabel=${heading}
|
||||
rootTabbable
|
||||
dialogInitialFocus
|
||||
>
|
||||
<ha-md-list-item @click=${this._automatic} type="button">
|
||||
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
|
||||
<span slot="headline">Use automatic backup settings</span>
|
||||
<span slot="supporting-text">
|
||||
Trigger a backup using the configured settings for automatic backups
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item @click=${this._manual} type="button">
|
||||
<ha-svg-icon slot="start" .path=${mdiCogs}></ha-svg-icon>
|
||||
|
||||
<span slot="headline"> Create a manual backup</span>
|
||||
<span slot="supporting-text">
|
||||
Create a backup with custom settings (e.g. specific add-ons,
|
||||
database, etc.)
|
||||
</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _manual() {
|
||||
this._params!.submit?.("manual");
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _automatic() {
|
||||
this._params!.submit?.("automatic");
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
max-width: 500px;
|
||||
}
|
||||
div[slot="content"] {
|
||||
margin-top: -16px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
max-width: none;
|
||||
}
|
||||
div[slot="content"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
}
|
||||
ha-md-list-item {
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-new-backup": DialogNewBackup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export interface GenerateBackupDialogParams {
|
||||
submit?: (response: { backup_id: string }) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadGenerateBackupDialog = () =>
|
||||
import("./dialog-generate-backup");
|
||||
|
||||
export const showGenerateBackupDialog = (
|
||||
element: HTMLElement,
|
||||
params: GenerateBackupDialogParams
|
||||
) =>
|
||||
new Promise<{ backup_id: string } | null>((resolve) => {
|
||||
const origCancel = params.cancel;
|
||||
const origSubmit = params.submit;
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-generate-backup",
|
||||
dialogImport: loadGenerateBackupDialog,
|
||||
dialogParams: {
|
||||
...params,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (response) => {
|
||||
resolve(response);
|
||||
if (origSubmit) {
|
||||
origSubmit(response);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
37
src/panels/config/backup/dialogs/show-dialog-new-backup.ts
Normal file
37
src/panels/config/backup/dialogs/show-dialog-new-backup.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export type NewBackupType = "automatic" | "manual";
|
||||
export interface NewBackupDialogParams {
|
||||
submit?: (type: NewBackupType) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadNewBackupDialog = () => import("./dialog-new-backup");
|
||||
|
||||
export const showNewBackupDialog = (
|
||||
element: HTMLElement,
|
||||
params: NewBackupDialogParams
|
||||
) =>
|
||||
new Promise<NewBackupType | null>((resolve) => {
|
||||
const origCancel = params.cancel;
|
||||
const origSubmit = params.submit;
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-new-backup",
|
||||
dialogImport: loadNewBackupDialog,
|
||||
dialogParams: {
|
||||
...params,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (response) => {
|
||||
resolve(response);
|
||||
if (origSubmit) {
|
||||
origSubmit(response);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
132
src/panels/config/backup/ha-config-backup-automatic-config.ts
Normal file
132
src/panels/config/backup/ha-config-backup-automatic-config.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-config-backup-automatic-config")
|
||||
class HaConfigBackupAutomaticConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${"Automatic backups"}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-header">Automation</div>
|
||||
<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Schedule</span>
|
||||
<span slot="description">
|
||||
How often you want to create a backup.
|
||||
</span>
|
||||
<ha-select naturalMenuWidth>
|
||||
<ha-list-item>Daily at 02:00</ha-list-item>
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Maximum copies</span>
|
||||
<span slot="description">
|
||||
The number of backups that are saved
|
||||
</span>
|
||||
<ha-select naturalMenuWidth>
|
||||
<ha-list-item>Latest 3 copies</ha-list-item>
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Locations</span>
|
||||
<span slot="description">
|
||||
What locations you want to automatically backup to.
|
||||
</span>
|
||||
<ha-button> Configure </ha-button>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Password</span>
|
||||
<span slot="description">
|
||||
Automatic backups are protected with this password
|
||||
</span>
|
||||
<ha-switch></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Custom backup name</span>
|
||||
<span slot="description">
|
||||
By default it will use the date and description (2024-07-05
|
||||
Automatic backup).
|
||||
</span>
|
||||
<ha-switch></ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-header">Backup data</div>
|
||||
<div class="card-content">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
Home Assistant settings is always included
|
||||
</span>
|
||||
<span slot="description">
|
||||
With these settings you are able to restore your system.
|
||||
</span>
|
||||
<ha-button>Learn more</ha-button>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">History</span>
|
||||
<span slot="description">
|
||||
For example of your energy dashboard.
|
||||
</span>
|
||||
<ha-switch></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Media</span>
|
||||
<span slot="description">For example camera recordings.</span>
|
||||
<ha-switch></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Add-ons</span>
|
||||
<span slot="description">
|
||||
Select what add-ons you want to backup.
|
||||
</span>
|
||||
<ha-select naturalMenuWidth>
|
||||
<ha-list-item>All, including new (4)</ha-list-item>
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-automatic-config": HaConfigBackupAutomaticConfig;
|
||||
}
|
||||
}
|
||||
424
src/panels/config/backup/ha-config-backup-dashboard.ts
Normal file
424
src/panels/config/backup/ha-config-backup-dashboard.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
fetchBackupInfo,
|
||||
getBackupDownloadUrl,
|
||||
removeBackup,
|
||||
type BackupContent,
|
||||
getPreferredAgentForDownload,
|
||||
} from "../../../data/backup";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "./components/ha-backup-summary-card";
|
||||
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||
|
||||
@customElement("ha-config-backup-dashboard")
|
||||
class HaConfigBackupDashboard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _backingUp = false;
|
||||
|
||||
@state() private _backups: BackupContent[] = [];
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<BackupContent> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
template: (backup) => backup.name,
|
||||
},
|
||||
size: {
|
||||
title: localize("ui.panel.config.backup.size"),
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
date: {
|
||||
title: localize("ui.panel.config.backup.created"),
|
||||
direction: "desc",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
},
|
||||
locations: {
|
||||
title: "Locations",
|
||||
template: (backup) =>
|
||||
html`${(backup.agent_ids || []).map((agent) => {
|
||||
const [domain, name] = agent.split(".");
|
||||
return html`
|
||||
<img
|
||||
title=${name}
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
height="24"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${name}
|
||||
slot="graphic"
|
||||
/>
|
||||
`;
|
||||
})}`,
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
type: "overflow-menu",
|
||||
template: (backup) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
label: this.hass.localize("ui.common.download"),
|
||||
path: mdiDownload,
|
||||
action: () => this._downloadBackup(backup),
|
||||
},
|
||||
{
|
||||
label: this.hass.localize("ui.common.delete"),
|
||||
path: mdiDelete,
|
||||
action: () => this._deleteBackup(backup),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
hasFab
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
path: `/config/backup/list`,
|
||||
},
|
||||
]}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
clickable
|
||||
id="backup_id"
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.route=${this.route}
|
||||
@row-click=${this._showBackupDetails}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._backups ?? []}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.backup.picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="top_header" class="header">
|
||||
<ha-backup-summary-card
|
||||
title="Automatically backed up"
|
||||
description="Your configuration has been backed up."
|
||||
has-action
|
||||
.status=${this._backingUp ? "loading" : "success"}
|
||||
>
|
||||
<ha-button slot="action" @click=${this._configureAutomaticBackup}>
|
||||
Configure
|
||||
</ha-button>
|
||||
</ha-backup-summary-card>
|
||||
<ha-backup-summary-card
|
||||
title="3 automatic backup locations"
|
||||
description="One is off-site"
|
||||
has-action
|
||||
.status=${"success"}
|
||||
>
|
||||
<ha-button slot="action" @click=${this._configureBackupLocations}>
|
||||
Configure
|
||||
</ha-button>
|
||||
</ha-backup-summary-card>
|
||||
</div>
|
||||
|
||||
${this._selected.length
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this._selected.length} backups selected
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-button @click=${this._deleteSelected} class="warning">
|
||||
Delete selected
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${"Delete selected"}
|
||||
.path=${mdiDelete}
|
||||
id="delete-btn"
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
<simple-tooltip animation-delay="0" for="delete-btn">
|
||||
Delete selected
|
||||
</simple-tooltip>
|
||||
`}
|
||||
</div>
|
||||
</div> `
|
||||
: nothing}
|
||||
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${this._backingUp}
|
||||
.label=${this.hass.localize("ui.panel.config.backup.create_backup")}
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
private async _fetchBackupInfo() {
|
||||
const info = await fetchBackupInfo(this.hass);
|
||||
this._backups = info.backups;
|
||||
this._backingUp = info.backing_up;
|
||||
}
|
||||
|
||||
private async _newBackup(): Promise<void> {
|
||||
const type = await showNewBackupDialog(this, {});
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "manual") {
|
||||
await this._generateBackup();
|
||||
} else {
|
||||
// Todo: implement trigger automatic backup
|
||||
}
|
||||
}
|
||||
|
||||
private async _generateBackup(): Promise<void> {
|
||||
const response = await showGenerateBackupDialog(this, {});
|
||||
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._fetchBackupInfo();
|
||||
|
||||
// Todo subscribe for status updates instead of polling
|
||||
const interval = setInterval(async () => {
|
||||
await this._fetchBackupInfo();
|
||||
if (!this._backingUp) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private _showBackupDetails(ev: CustomEvent): void {
|
||||
const id = (ev.detail as RowClickedEvent).id;
|
||||
navigate(`/config/backup/details/${id}`);
|
||||
}
|
||||
|
||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||
const preferedAgent = getPreferredAgentForDownload(backup!.agent_ids!);
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
getBackupDownloadUrl(backup.backup_id, preferedAgent)
|
||||
);
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: "Delete backup",
|
||||
text: "This backup will be permanently deleted.",
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeBackup(this.hass, backup.backup_id);
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
private async _deleteSelected() {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: "Delete selected backups",
|
||||
text: "These backups will be permanently deleted.",
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
this._selected.map((slug) => removeBackup(this.hass, slug))
|
||||
);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to delete backups",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this._fetchBackupInfo();
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _configureAutomaticBackup() {
|
||||
navigate("/config/backup/automatic-config");
|
||||
}
|
||||
|
||||
private _configureBackupLocations() {
|
||||
navigate("/config/backup/locations");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.header {
|
||||
padding: 16px 16px 0 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.header > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
ha-fab[disabled] {
|
||||
--mdc-theme-secondary: var(--disabled-text-color) !important;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: var(--header-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
position: relative;
|
||||
top: -4px;
|
||||
}
|
||||
.selected-txt {
|
||||
font-weight: bold;
|
||||
padding-left: 16px;
|
||||
padding-inline-start: 16px;
|
||||
padding-inline-end: initial;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header .selected-txt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.header-toolbar .selected-txt {
|
||||
font-size: 16px;
|
||||
}
|
||||
.header-toolbar .header-btns {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header-btns > ha-button,
|
||||
.header-btns > ha-icon-button {
|
||||
margin: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-dashboard": HaConfigBackupDashboard;
|
||||
}
|
||||
}
|
||||
252
src/panels/config/backup/ha-config-backup-details.ts
Normal file
252
src/panels/config/backup/ha-config-backup-details.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDelete, mdiDotsVertical, mdiDownload } 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 { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { BackupContent } from "../../../data/backup";
|
||||
import {
|
||||
fetchBackupDetails,
|
||||
getBackupDownloadUrl,
|
||||
getPreferredAgentForDownload,
|
||||
removeBackup,
|
||||
} from "../../../data/backup";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
|
||||
|
||||
@customElement("ha-config-backup-details")
|
||||
class HaConfigBackupDetails extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "backup-id" }) public backupId!: string;
|
||||
|
||||
@state() private _backup?: BackupContent | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.backupId) {
|
||||
this._fetchBackup();
|
||||
} else {
|
||||
this._error = "Backup id not defined";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this._backup?.name || "Backup"}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon" @action=${this._handleAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.download")}
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" class="warning">
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
${this._error &&
|
||||
html`<ha-alert alert-type="error">${this._error}</ha-alert>`}
|
||||
${this._backup === null
|
||||
? html`<ha-alert alert-type="warning" title="Not found">
|
||||
Backup matching ${this.backupId} not found
|
||||
</ha-alert>`
|
||||
: !this._backup
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
<ha-card header="Backup">
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${Math.ceil(this._backup.size * 10) / 10 + " MB"}
|
||||
</span>
|
||||
<span slot="supporting-text">Size</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
${formatDateTime(
|
||||
new Date(this._backup.date),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
<span slot="supporting-text">Created</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card header="Locations">
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
${this._backup.agent_ids?.map((agent) => {
|
||||
const [domain, name] = agent.split(".");
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
domain
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
slot="start"
|
||||
/>
|
||||
<div slot="headline">${domainName}: ${name}</div>
|
||||
<ha-button-menu
|
||||
slot="end"
|
||||
@action=${this._handleAgentAction}
|
||||
.agent=${agent}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
Download from this location
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchBackup() {
|
||||
try {
|
||||
const response = await fetchBackupDetails(this.hass, this.backupId);
|
||||
this._backup = response.backup;
|
||||
} catch (err: any) {
|
||||
this._error = err?.message || "Could not fetch backup details";
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._downloadBackup();
|
||||
break;
|
||||
case 1:
|
||||
this._deleteBackup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAgentAction(ev: CustomEvent<ActionDetail>) {
|
||||
const button = ev.currentTarget;
|
||||
const agentId = (button as any).agent;
|
||||
this._downloadBackup(agentId);
|
||||
}
|
||||
|
||||
private async _downloadBackup(agentId?: string): Promise<void> {
|
||||
const preferedAgent =
|
||||
agentId ?? getPreferredAgentForDownload(this._backup!.agent_ids!);
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
getBackupDownloadUrl(this._backup!.backup_id, preferedAgent)
|
||||
);
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private async _deleteBackup(): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: "Delete backup",
|
||||
text: "This backup will be permanently deleted.",
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeBackup(this.hass, this._backup!.backup_id);
|
||||
navigate("/config/backup");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: grid;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0 20px 8px 20px;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
width: 48px;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.warning ha-svg-icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-details": HaConfigBackupDetails;
|
||||
}
|
||||
}
|
||||
138
src/panels/config/backup/ha-config-backup-locations.ts
Normal file
138
src/panels/config/backup/ha-config-backup-locations.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import type { BackupAgent } from "../../../data/backup";
|
||||
import { fetchBackupAgentsInfo } from "../../../data/backup";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
|
||||
@customElement("ha-config-backup-locations")
|
||||
class HaConfigBackupLocations extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _agents: BackupAgent[] = [];
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchAgents();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/backup"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.caption")}
|
||||
>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<h2 class="title">Locations</h2>
|
||||
<p class="description">
|
||||
To keep your data safe it is recommended your backups is at least
|
||||
on two different locations and one of them is off-site.
|
||||
</p>
|
||||
</div>
|
||||
<ha-card class="agents">
|
||||
<div class="card-content">
|
||||
${this._agents.length > 0
|
||||
? html`
|
||||
<ha-md-list>
|
||||
${this._agents.map((agent) => {
|
||||
const [domain, name] = agent.agent_id.split(".");
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
domain
|
||||
);
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
type="link"
|
||||
href="/config/backup/locations/${agent.agent_id}"
|
||||
>
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=""
|
||||
slot="start"
|
||||
/>
|
||||
<div slot="headline">${domainName}: ${name}</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-md-list>
|
||||
`
|
||||
: html`<p>No sync agents configured</p>`}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchAgents() {
|
||||
const data = await fetchBackupAgentsInfo(this.hass);
|
||||
this._agents = data.agents;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header .title {
|
||||
font-size: 22px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
color: var(--primary-text-color);
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header .description {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
color: var(--secondary-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
width: 48px;
|
||||
}
|
||||
.card-content {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-backup-locations": HaConfigBackupLocations;
|
||||
}
|
||||
}
|
||||
@@ -1,235 +1,49 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import type { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import type { BackupContent, BackupData } from "../../../data/backup";
|
||||
import {
|
||||
fetchBackupInfo,
|
||||
generateBackup,
|
||||
getBackupDownloadUrl,
|
||||
removeBackup,
|
||||
} from "../../../data/backup";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./ha-config-backup-dashboard";
|
||||
|
||||
@customElement("ha-config-backup")
|
||||
class HaConfigBackup extends LitElement {
|
||||
class HaConfigBackup extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _backupData?: BackupData;
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BackupContent> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
routes: {
|
||||
dashboard: {
|
||||
tag: "ha-config-backup-dashboard",
|
||||
cache: true,
|
||||
},
|
||||
path: {
|
||||
title: localize("ui.panel.config.backup.path"),
|
||||
hidden: !narrow,
|
||||
details: {
|
||||
tag: "ha-config-backup-details",
|
||||
load: () => import("./ha-config-backup-details"),
|
||||
},
|
||||
size: {
|
||||
title: localize("ui.panel.config.backup.size"),
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
locations: {
|
||||
tag: "ha-config-backup-locations",
|
||||
load: () => import("./ha-config-backup-locations"),
|
||||
},
|
||||
date: {
|
||||
title: localize("ui.panel.config.backup.created"),
|
||||
direction: "desc",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
relativeTime(new Date(backup.date), this.hass.locale),
|
||||
"automatic-config": {
|
||||
tag: "ha-config-backup-automatic-config",
|
||||
load: () => import("./ha-config-backup-automatic-config"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actions: {
|
||||
title: "",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (backup) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.items=${[
|
||||
// Download Button
|
||||
{
|
||||
path: mdiDownload,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.download_backup"
|
||||
),
|
||||
action: () => this._downloadBackup(backup),
|
||||
},
|
||||
// Delete button
|
||||
{
|
||||
path: mdiDelete,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.backup.remove_backup"
|
||||
),
|
||||
action: () => this._removeBackup(backup),
|
||||
},
|
||||
]}
|
||||
style="color: var(--secondary-text-color)"
|
||||
>
|
||||
</ha-icon-overflow-menu>`,
|
||||
},
|
||||
})
|
||||
);
|
||||
protected updatePageEl(pageEl, changedProps: PropertyValues) {
|
||||
pageEl.hass = this.hass;
|
||||
pageEl.route = this.routeTail;
|
||||
|
||||
private _getItems = memoize((backupItems: BackupContent[]) =>
|
||||
backupItems.map((backup) => ({
|
||||
name: backup.name,
|
||||
slug: backup.slug,
|
||||
date: backup.date,
|
||||
size: backup.size,
|
||||
path: backup.path,
|
||||
}))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || this._backupData === undefined) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
if (
|
||||
(!changedProps || changedProps.has("route")) &&
|
||||
this._currentPage === "details"
|
||||
) {
|
||||
pageEl.backupId = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
hasFab
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
path: `/config/backup`,
|
||||
},
|
||||
]}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.backup.picker.search"
|
||||
)}
|
||||
>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${this._backupData.backing_up}
|
||||
.label=${this._backupData.backing_up
|
||||
? this.hass.localize("ui.panel.config.backup.creating_backup")
|
||||
: this.hass.localize("ui.panel.config.backup.create_backup")}
|
||||
extended
|
||||
@click=${this._generateBackup}
|
||||
>
|
||||
${this._backupData.backing_up
|
||||
? html`<ha-circular-progress
|
||||
slot="icon"
|
||||
indeterminate
|
||||
></ha-circular-progress>`
|
||||
: html`<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._getBackups();
|
||||
}
|
||||
|
||||
private async _getBackups(): Promise<void> {
|
||||
this._backupData = await fetchBackupInfo(this.hass);
|
||||
}
|
||||
|
||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
getBackupDownloadUrl(backup.slug)
|
||||
);
|
||||
fileDownload(signedUrl.path);
|
||||
}
|
||||
|
||||
private async _generateBackup(): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.backup.create.title"),
|
||||
text: this.hass.localize("ui.panel.config.backup.create.description"),
|
||||
confirmText: this.hass.localize("ui.panel.config.backup.create.confirm"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateBackup(this.hass)
|
||||
.then(() => this._getBackups())
|
||||
.catch((err) => showAlertDialog(this, { text: (err as Error).message }));
|
||||
|
||||
await this._getBackups();
|
||||
}
|
||||
|
||||
private async _removeBackup(backup: BackupContent): Promise<void> {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.backup.remove.title"),
|
||||
text: this.hass.localize("ui.panel.config.backup.remove.description", {
|
||||
name: backup.name,
|
||||
}),
|
||||
confirmText: this.hass.localize("ui.panel.config.backup.remove.confirm"),
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await removeBackup(this.hass, backup.slug);
|
||||
await this._getBackups();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
ha-fab[disabled] {
|
||||
--mdc-theme-secondary: var(--disabled-text-color) !important;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export class DialogJoinBeta
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.backup")}
|
||||
</ha-alert>
|
||||
<p>
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.warning")}
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.warning")}.<br />
|
||||
${this.hass.localize("ui.dialogs.join_beta_channel.release_items")}
|
||||
</p>
|
||||
<ul>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
|
||||
@@ -86,36 +87,42 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-card outlined .header=${this.header}>
|
||||
<div id="entities">
|
||||
<mwc-list>
|
||||
${shownEntities.map((entry) =>
|
||||
this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry)
|
||||
)}
|
||||
</mwc-list>
|
||||
</div>
|
||||
${hiddenEntities.length
|
||||
? !this.showHidden
|
||||
? html`
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hidden_entities",
|
||||
{ count: hiddenEntities.length }
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
${shownEntities.length
|
||||
? html`
|
||||
<div id="entities" class="move-up">
|
||||
<mwc-list>
|
||||
${hiddenEntities.map((entry) => this._renderEntry(entry))}
|
||||
</mwc-list>
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.show_less"
|
||||
${shownEntities.map((entry) =>
|
||||
this.hass.states[entry.entity_id]
|
||||
? this._renderEntity(entry)
|
||||
: this._renderEntry(entry)
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
</mwc-list>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${hiddenEntities.length
|
||||
? html`<div class=${classMap({ "move-up": !shownEntities.length })}>
|
||||
${!this.showHidden
|
||||
? html`
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.hidden_entities",
|
||||
{ count: hiddenEntities.length }
|
||||
)}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
<mwc-list>
|
||||
${hiddenEntities.map((entry) => this._renderEntry(entry))}
|
||||
</mwc-list>
|
||||
<button class="show-more" @click=${this._toggleShowHidden}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.entities.show_less"
|
||||
)}
|
||||
</button>
|
||||
`}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._addToLovelaceView}>
|
||||
${this.hass.localize(
|
||||
@@ -257,8 +264,8 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||
.disabled-entry {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
#entities {
|
||||
margin-top: -24px; /* match the spacing between card title and content of the device info card above it */
|
||||
.move-up {
|
||||
margin-top: -24px;
|
||||
}
|
||||
#entities > * {
|
||||
margin: 8px 16px 8px 8px;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
mdiHospitalBox,
|
||||
mdiInformation,
|
||||
mdiUpload,
|
||||
mdiWrench,
|
||||
} from "@mdi/js";
|
||||
import { getConfigEntries } from "../../../../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
@@ -98,6 +99,13 @@ export const getZwaveDeviceActions = async (
|
||||
showZWaveJSNodeStatisticsDialog(el, {
|
||||
device,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: hass.localize(
|
||||
"ui.panel.config.zwave_js.device_info.installer_settings"
|
||||
),
|
||||
icon: mdiWrench,
|
||||
href: `/config/zwave_js/node_installer/${device.id}?config_entry=${entryId}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import {
|
||||
isDeletableEntity,
|
||||
deleteEntity,
|
||||
} from "../../../common/entity/delete_entity";
|
||||
import type { Helper } from "../helpers/const";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import { HELPERS_CRUD } from "../../../data/helpers_crud";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
@@ -73,12 +80,15 @@ import type {
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
computeEntityRegistryName,
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
fetchIntegrationManifests,
|
||||
domainToName,
|
||||
} from "../../../data/integration";
|
||||
import type { EntitySources } from "../../../data/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../../data/label_registry";
|
||||
import {
|
||||
createLabelRegistryEntry,
|
||||
@@ -136,6 +146,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entries?: ConfigEntry[];
|
||||
|
||||
@state() private _manifests?: IntegrationManifest[];
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entities!: EntityRegistryEntry[];
|
||||
@@ -1280,11 +1292,46 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _removeSelected() {
|
||||
const removeableEntities = this._selected.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
return stateObj?.attributes.restored;
|
||||
private async _removeSelected() {
|
||||
if (!this._entities || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const manifestsProm = this._manifests
|
||||
? undefined
|
||||
: fetchIntegrationManifests(this.hass);
|
||||
const helperDomains = [
|
||||
...new Set(this._selected.map((s) => computeDomain(s))),
|
||||
].filter((d) => isHelperDomain(d));
|
||||
|
||||
const configEntriesProm = this._entries
|
||||
? undefined
|
||||
: this._loadConfigEntries();
|
||||
const domainProms = helperDomains.map((d) =>
|
||||
HELPERS_CRUD[d].fetch(this.hass)
|
||||
);
|
||||
const helpersResult = await Promise.all(domainProms);
|
||||
let fetchedHelpers: Helper[] = [];
|
||||
helpersResult.forEach((r) => {
|
||||
fetchedHelpers = fetchedHelpers.concat(r);
|
||||
});
|
||||
if (manifestsProm) {
|
||||
this._manifests = await manifestsProm;
|
||||
}
|
||||
if (configEntriesProm) {
|
||||
await configEntriesProm;
|
||||
}
|
||||
|
||||
const removeableEntities = this._selected.filter((entity_id) =>
|
||||
isDeletableEntity(
|
||||
this.hass,
|
||||
entity_id,
|
||||
this._manifests!,
|
||||
this._entities,
|
||||
this._entries!,
|
||||
fetchedHelpers
|
||||
)
|
||||
);
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.entities.picker.delete_selected.confirm_title`
|
||||
@@ -1305,8 +1352,15 @@ ${rejected
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
removeableEntities.forEach((entity) =>
|
||||
removeEntityRegistryEntry(this.hass, entity)
|
||||
removeableEntities.forEach((entity_id) =>
|
||||
deleteEntity(
|
||||
this.hass,
|
||||
entity_id,
|
||||
this._manifests!,
|
||||
this._entities,
|
||||
this._entries!,
|
||||
fetchedHelpers
|
||||
)
|
||||
);
|
||||
this._clearSelection();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
@@ -13,19 +14,6 @@ import type {
|
||||
} from "./show-dialog-schedule-block-info";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "from",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
];
|
||||
|
||||
class DialogScheduleBlockInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -35,10 +23,39 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
|
||||
@state() private _params?: ScheduleBlockInfoDialogParams;
|
||||
|
||||
private _expand = false;
|
||||
|
||||
private _schema = memoizeOne((expand: boolean) => [
|
||||
{
|
||||
name: "from",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
{
|
||||
name: "advanced_settings",
|
||||
type: "expandable" as const,
|
||||
flatten: true,
|
||||
expanded: expand,
|
||||
schema: [
|
||||
{
|
||||
name: "data",
|
||||
required: false,
|
||||
selector: { object: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
public showDialog(params: ScheduleBlockInfoDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._data = params.block;
|
||||
this._expand = !!params.block?.data;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@@ -66,7 +83,7 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
<div>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(this._expand)}
|
||||
.data=${this._data}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -110,12 +127,20 @@ class DialogScheduleBlockInfo extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "from":
|
||||
return this.hass!.localize("ui.dialogs.helper_settings.schedule.start");
|
||||
case "to":
|
||||
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
|
||||
case "data":
|
||||
return this.hass!.localize("ui.dialogs.helper_settings.schedule.data");
|
||||
case "advanced_settings":
|
||||
return this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.schedule.advanced_settings"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
export interface ScheduleBlockInfo {
|
||||
from: string;
|
||||
to: string;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ScheduleBlockInfoDialogParams {
|
||||
|
||||
@@ -3,19 +3,23 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
mdiPencilOff,
|
||||
mdiProgressHelper,
|
||||
mdiPlus,
|
||||
mdiTag,
|
||||
mdiTrashCan,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -54,7 +58,11 @@ import {
|
||||
subscribeCategoryRegistry,
|
||||
} from "../../../data/category_registry";
|
||||
import type { ConfigEntry } from "../../../data/config_entries";
|
||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||
import {
|
||||
ERROR_STATES,
|
||||
deleteConfigEntry,
|
||||
subscribeConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type {
|
||||
@@ -97,6 +105,7 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { renderConfigEntryError } from "../integrations/ha-config-integration-page";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { isHelperDomain } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
@@ -220,6 +229,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
private _debouncedFetchEntitySources = debounce(
|
||||
() => this._fetchEntitySources(),
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeConfigEntries(
|
||||
@@ -236,6 +251,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
} else if (message.type === "updated") {
|
||||
newEntries[message.entry.entry_id] = message.entry;
|
||||
}
|
||||
if (
|
||||
this._entitySource &&
|
||||
this._configEntries &&
|
||||
message.entry.state === "loaded" &&
|
||||
this._configEntries[message.entry.entry_id]?.state !== "loaded"
|
||||
) {
|
||||
this._debouncedFetchEntitySources();
|
||||
}
|
||||
});
|
||||
this._configEntries = newEntries;
|
||||
},
|
||||
@@ -352,6 +375,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
...(helper.configEntry &&
|
||||
ERROR_STATES.includes(helper.configEntry.state)
|
||||
? [
|
||||
{
|
||||
path: mdiAlertCircle,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.error_information"
|
||||
),
|
||||
warning: true,
|
||||
action: () => this._showError(helper),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
path: mdiCog,
|
||||
label: this.hass.localize(
|
||||
@@ -366,6 +402,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
action: () => this._editCategory(helper),
|
||||
},
|
||||
...(helper.configEntry &&
|
||||
helper.editable &&
|
||||
ERROR_STATES.includes(helper.configEntry.state) &&
|
||||
helper.entity === undefined
|
||||
? [
|
||||
{
|
||||
path: mdiTrashCan,
|
||||
label: this.hass.localize("ui.common.delete"),
|
||||
warning: true,
|
||||
action: () => this._deleteEntry(helper),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
@@ -417,17 +466,27 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
});
|
||||
|
||||
const entries = Object.values(configEntriesCopy).map((configEntry) => ({
|
||||
id: configEntry.entry_id,
|
||||
entity_id: "",
|
||||
icon: mdiAlertCircle,
|
||||
name: configEntry.title || "",
|
||||
editable: true,
|
||||
type: configEntry.domain,
|
||||
configEntry,
|
||||
entity: undefined,
|
||||
selectable: false,
|
||||
}));
|
||||
const entries = Object.values(configEntriesCopy).map((configEntry) => {
|
||||
const entityEntry = Object.values(entityEntries).find(
|
||||
(entry) => entry.config_entry_id === configEntry.entry_id
|
||||
);
|
||||
const entityIsDisabled = !!entityEntry?.disabled_by;
|
||||
return {
|
||||
id: entityIsDisabled ? entityEntry.entity_id : configEntry.entry_id,
|
||||
entity_id: entityIsDisabled ? entityEntry.entity_id : "",
|
||||
icon: entityIsDisabled
|
||||
? mdiCancel
|
||||
: configEntry.state === "setup_in_progress"
|
||||
? mdiProgressHelper
|
||||
: mdiAlertCircle,
|
||||
name: configEntry.title || "",
|
||||
editable: true,
|
||||
type: configEntry.domain,
|
||||
configEntry,
|
||||
entity: undefined,
|
||||
selectable: entityIsDisabled,
|
||||
};
|
||||
});
|
||||
|
||||
return [...states, ...entries]
|
||||
.filter((item) =>
|
||||
@@ -1081,6 +1140,34 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private _showError(helper: HelperItem) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.errors.config.configuration_error"),
|
||||
text: renderConfigEntryError(this.hass, helper.configEntry!),
|
||||
warning: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _deleteEntry(helper: HelperItem) {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
{ title: helper.configEntry!.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;
|
||||
}
|
||||
deleteConfigEntry(this.hass, helper.id);
|
||||
}
|
||||
|
||||
private _openSettings(helper: HelperItem) {
|
||||
if (helper.entity) {
|
||||
showMoreInfoDialog(this, {
|
||||
|
||||
@@ -106,6 +106,38 @@ import { fileDownload } from "../../../util/file_download";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
|
||||
export const renderConfigEntryError = (
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry
|
||||
): TemplateResult => {
|
||||
if (entry.reason) {
|
||||
if (entry.error_reason_translation_key) {
|
||||
const lokalisePromExc = hass
|
||||
.loadBackendTranslation("exceptions", entry.domain)
|
||||
.then(
|
||||
(localize) =>
|
||||
localize(
|
||||
`component.${entry.domain}.exceptions.${entry.error_reason_translation_key}.message`,
|
||||
entry.error_reason_translation_placeholders ?? undefined
|
||||
) || entry.reason
|
||||
);
|
||||
return html`${until(lokalisePromExc)}`;
|
||||
}
|
||||
const lokalisePromError = hass
|
||||
.loadBackendTranslation("config", entry.domain)
|
||||
.then(
|
||||
(localize) =>
|
||||
localize(`component.${entry.domain}.config.error.${entry.reason}`) ||
|
||||
entry.reason
|
||||
);
|
||||
return html`${until(lokalisePromError, entry.reason)}`;
|
||||
}
|
||||
return html`
|
||||
<br />
|
||||
${hass.localize("ui.panel.config.integrations.config_entry.check_the_logs")}
|
||||
`;
|
||||
};
|
||||
|
||||
@customElement("ha-config-integration-page")
|
||||
class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -618,37 +650,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
stateText = [
|
||||
`ui.panel.config.integrations.config_entry.state.${item.state}`,
|
||||
];
|
||||
if (item.reason) {
|
||||
if (item.error_reason_translation_key) {
|
||||
const lokalisePromExc = this.hass
|
||||
.loadBackendTranslation("exceptions", item.domain)
|
||||
.then(
|
||||
(localize) =>
|
||||
localize(
|
||||
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
|
||||
item.error_reason_translation_placeholders ?? undefined
|
||||
) || item.reason
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromExc)}`;
|
||||
} else {
|
||||
const lokalisePromError = this.hass
|
||||
.loadBackendTranslation("config", item.domain)
|
||||
.then(
|
||||
(localize) =>
|
||||
localize(
|
||||
`component.${item.domain}.config.error.${item.reason}`
|
||||
) || item.reason
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromError, item.reason)}`;
|
||||
}
|
||||
} else {
|
||||
stateTextExtra = html`
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.check_the_logs"
|
||||
)}
|
||||
`;
|
||||
}
|
||||
stateTextExtra = renderConfigEntryError(this.hass, item);
|
||||
}
|
||||
|
||||
const devices = this._getConfigEntryDevices(item);
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { invokeZWaveCCApi } from "../../../../../../data/zwave_js";
|
||||
import "../../../../../../components/ha-alert";
|
||||
import "../../../../../../components/ha-circular-progress";
|
||||
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
|
||||
import "./zwave_js-capability-control-multilevel-switch";
|
||||
|
||||
enum ColorComponent {
|
||||
"Warm White" = 0,
|
||||
"Cold White",
|
||||
Red,
|
||||
Green,
|
||||
Blue,
|
||||
Amber,
|
||||
Cyan,
|
||||
Purple,
|
||||
Index,
|
||||
}
|
||||
|
||||
@customElement("zwave_js-capability-control-color_switch")
|
||||
class ZWaveJSCapabilityColorSwitch extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ type: Number }) public endpoint!: number;
|
||||
|
||||
@property({ type: Number }) public command_class!: number;
|
||||
|
||||
@property({ type: Number }) public version!: number;
|
||||
|
||||
@state() private _color_components?: ColorComponent[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
if (!this._color_components) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
return this._color_components.map(
|
||||
(color) =>
|
||||
html` <h5>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.color_component"
|
||||
)}:
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.color_switch.colors.${color}`
|
||||
)}
|
||||
</h5>
|
||||
<zwave_js-capability-control-multilevel_switch
|
||||
.hass=${this.hass}
|
||||
.device=${this.device}
|
||||
.endpoint=${this.endpoint}
|
||||
.command_class=${this.command_class}
|
||||
.version=${this.version}
|
||||
.transform_options=${this._transformOptions(color)}
|
||||
></zwave_js-capability-control-multilevel_switch>`
|
||||
);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
try {
|
||||
this._color_components = (await invokeZWaveCCApi(
|
||||
this.hass,
|
||||
this.device.id,
|
||||
this.command_class,
|
||||
this.endpoint,
|
||||
"getSupported",
|
||||
[],
|
||||
true
|
||||
)) as number[];
|
||||
} catch (error) {
|
||||
this._error = extractApiErrorMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
private _transformOptions(color: number) {
|
||||
return (opts: Record<string, any>, control: string) =>
|
||||
control === "startLevelChange"
|
||||
? {
|
||||
...opts,
|
||||
colorComponent: color,
|
||||
}
|
||||
: color;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-capability-control-color_switch": ZWaveJSCapabilityColorSwitch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../../components/buttons/ha-progress-button";
|
||||
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { invokeZWaveCCApi } from "../../../../../../data/zwave_js";
|
||||
import "../../../../../../components/ha-textfield";
|
||||
import "../../../../../../components/ha-select";
|
||||
import "../../../../../../components/ha-list-item";
|
||||
import "../../../../../../components/ha-alert";
|
||||
import "../../../../../../components/ha-formfield";
|
||||
import "../../../../../../components/ha-switch";
|
||||
import type { HaProgressButton } from "../../../../../../components/buttons/ha-progress-button";
|
||||
import type { HaSelect } from "../../../../../../components/ha-select";
|
||||
import type { HaTextField } from "../../../../../../components/ha-textfield";
|
||||
import type { HaSwitch } from "../../../../../../components/ha-switch";
|
||||
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
|
||||
|
||||
@customElement("zwave_js-capability-control-multilevel_switch")
|
||||
class ZWaveJSCapabilityMultiLevelSwitch extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ type: Number }) public endpoint!: number;
|
||||
|
||||
@property({ type: Number }) public command_class!: number;
|
||||
|
||||
@property({ type: Number }) public version!: number;
|
||||
|
||||
@property({ attribute: false }) public transform_options?: (
|
||||
opts: Record<string, any>,
|
||||
control: string
|
||||
) => unknown;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.title"
|
||||
)}
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.direction"
|
||||
)}
|
||||
id="direction"
|
||||
>
|
||||
<ha-list-item .value=${"up"} selected
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.up"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item .value=${"down"}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.down"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-select>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.ignore_start_level"
|
||||
)}
|
||||
>
|
||||
<ha-switch id="ignore_start_level"></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-textfield
|
||||
type="number"
|
||||
id="start_level"
|
||||
value="0"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.start_level"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<div class="actions">
|
||||
<ha-progress-button
|
||||
.control=${"startLevelChange"}
|
||||
@click=${this._controlTransition}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.start_transition"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
.control=${"stopLevelChange"}
|
||||
@click=${this._controlTransition}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.stop_transition"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _controlTransition(ev: any) {
|
||||
const control = ev.currentTarget!.control;
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
button.progress = true;
|
||||
|
||||
const direction = (this.shadowRoot!.getElementById("direction") as HaSelect)
|
||||
.value;
|
||||
|
||||
const ignoreStartLevel = (
|
||||
this.shadowRoot!.getElementById("ignore_start_level") as HaSwitch
|
||||
).checked;
|
||||
|
||||
const startLevel = Number(
|
||||
(this.shadowRoot!.getElementById("start_level") as HaTextField).value
|
||||
);
|
||||
|
||||
const options = {
|
||||
direction,
|
||||
ignoreStartLevel,
|
||||
startLevel,
|
||||
};
|
||||
|
||||
try {
|
||||
button.actionSuccess();
|
||||
await invokeZWaveCCApi(
|
||||
this.hass,
|
||||
this.device.id,
|
||||
this.command_class,
|
||||
this.endpoint,
|
||||
control,
|
||||
[
|
||||
this.transform_options
|
||||
? this.transform_options(options, control)
|
||||
: options,
|
||||
],
|
||||
true
|
||||
);
|
||||
} catch (err) {
|
||||
button.actionError();
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.control_failed",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select,
|
||||
ha-formfield,
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-capability-control-multilevel_switch": ZWaveJSCapabilityMultiLevelSwitch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { invokeZWaveCCApi } from "../../../../../../data/zwave_js";
|
||||
import "../../../../../../components/ha-button";
|
||||
import "../../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../../components/ha-textfield";
|
||||
import "../../../../../../components/ha-select";
|
||||
import "../../../../../../components/ha-list-item";
|
||||
import "../../../../../../components/ha-alert";
|
||||
import type { HaSelect } from "../../../../../../components/ha-select";
|
||||
import type { HaTextField } from "../../../../../../components/ha-textfield";
|
||||
import { extractApiErrorMessage } from "../../../../../../data/hassio/common";
|
||||
import type { HaProgressButton } from "../../../../../../components/buttons/ha-progress-button";
|
||||
|
||||
// enum with special states
|
||||
enum SpecialState {
|
||||
frost_protection = "Frost Protection",
|
||||
energy_saving = "Energy Saving",
|
||||
unused = "Unused",
|
||||
}
|
||||
|
||||
const SETBACK_TYPE_OPTIONS = ["none", "temporary", "permanent"];
|
||||
|
||||
@customElement("zwave_js-capability-control-thermostat_setback")
|
||||
class ZWaveJSCapabilityThermostatSetback extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ type: Number }) public endpoint!: number;
|
||||
|
||||
@property({ type: Number }) public command_class!: number;
|
||||
|
||||
@property({ type: Number }) public version!: number;
|
||||
|
||||
@state() private _disableSetbackState = false;
|
||||
|
||||
@query("#setback_type") private _setbackTypeInput!: HaSelect;
|
||||
|
||||
@query("#setback_state") private _setbackStateInput!: HaTextField;
|
||||
|
||||
@query("#setback_special_state")
|
||||
private _setbackSpecialStateSelect!: HaSelect;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _loading = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.title`
|
||||
)}
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.label`
|
||||
)}
|
||||
id="setback_type"
|
||||
.value=${"0"}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${SETBACK_TYPE_OPTIONS.map(
|
||||
(translationKey, index) =>
|
||||
html`<ha-list-item .value=${String(index)}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.${translationKey}`
|
||||
)}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
<div class="setback-state">
|
||||
<ha-textfield
|
||||
type="number"
|
||||
id="setback_state"
|
||||
value="0"
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_state_label`
|
||||
)}
|
||||
min="-12.8"
|
||||
max="12.0"
|
||||
step=".1"
|
||||
.helper=${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_state_helper`
|
||||
)}
|
||||
.disabled=${this._disableSetbackState || this._loading}
|
||||
></ha-textfield>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.label`
|
||||
)}
|
||||
id="setback_special_state"
|
||||
@change=${this._changeSpecialState}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
<ha-list-item selected> </ha-list-item>
|
||||
${Object.entries(SpecialState).map(
|
||||
([translationKey, value]) =>
|
||||
html`<ha-list-item .value=${value}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.${translationKey}`
|
||||
)}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ha-button
|
||||
class="clear-button"
|
||||
@click=${this._clear}
|
||||
.disabled=${this._loading}
|
||||
>${this.hass.localize("ui.common.clear")}</ha-button
|
||||
>
|
||||
<ha-progress-button
|
||||
@click=${this._saveSetback}
|
||||
.disabled=${this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadSetback();
|
||||
}
|
||||
|
||||
private async _loadSetback() {
|
||||
this._loading = true;
|
||||
try {
|
||||
const { setbackType, setbackState } = (await invokeZWaveCCApi(
|
||||
this.hass,
|
||||
this.device.id,
|
||||
this.command_class,
|
||||
this.endpoint,
|
||||
"get",
|
||||
[],
|
||||
true
|
||||
)) as { setbackType: number; setbackState: number | SpecialState };
|
||||
|
||||
this._setbackTypeInput.value = String(setbackType);
|
||||
if (typeof setbackState === "number") {
|
||||
this._setbackStateInput.value = String(setbackState);
|
||||
this._setbackSpecialStateSelect.value = "";
|
||||
} else {
|
||||
this._setbackSpecialStateSelect.value = setbackState;
|
||||
}
|
||||
} catch (err) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.get_setback_failed",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
private _changeSpecialState() {
|
||||
this._disableSetbackState = !!this._setbackSpecialStateSelect.value;
|
||||
}
|
||||
|
||||
private async _saveSetback(ev: CustomEvent) {
|
||||
const button = ev.currentTarget as HaProgressButton;
|
||||
button.progress = true;
|
||||
|
||||
this._error = undefined;
|
||||
const setbackType = this._setbackTypeInput.value;
|
||||
|
||||
let setbackState: number | string = Number(this._setbackStateInput.value);
|
||||
if (this._setbackSpecialStateSelect.value) {
|
||||
setbackState = this._setbackSpecialStateSelect.value;
|
||||
}
|
||||
|
||||
try {
|
||||
await invokeZWaveCCApi(
|
||||
this.hass,
|
||||
this.device.id,
|
||||
this.command_class,
|
||||
this.endpoint,
|
||||
"set",
|
||||
[Number(setbackType), setbackState],
|
||||
true
|
||||
);
|
||||
|
||||
button.actionSuccess();
|
||||
} catch (err) {
|
||||
button.actionError();
|
||||
this._error = this.hass.localize(
|
||||
"ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.save_setback_failed",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
|
||||
button.progress = false;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this._loadSetback();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
:host > ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
.actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.actions .clear-button {
|
||||
--mdc-theme-primary: var(--red-color);
|
||||
}
|
||||
.setback-state {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.setback-state ha-select,
|
||||
ha-textfield {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-capability-control-thermostat_setback": ZWaveJSCapabilityThermostatSetback;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user