Compare commits
155 Commits
20241030.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 | ||
![]() |
5247b74fd4 | ||
![]() |
26e914290d | ||
![]() |
ed3096157c | ||
![]() |
04a45a4361 | ||
![]() |
5430040b96 | ||
![]() |
4bd70167ad | ||
![]() |
e908fbb48e | ||
![]() |
38da01abfa | ||
![]() |
c3b7ce8dc4 | ||
![]() |
0488d199ac | ||
![]() |
df3e4576db | ||
![]() |
6bd7788815 | ||
![]() |
9cdae4fea7 | ||
![]() |
7adf9f8526 | ||
![]() |
35dcb46703 | ||
![]() |
17db85ebad | ||
![]() |
fa39595c37 | ||
![]() |
4db908171f | ||
![]() |
7306b8c102 | ||
![]() |
928bf3465e | ||
![]() |
0b38143765 | ||
![]() |
2f974078e0 | ||
![]() |
efe90fcc55 | ||
![]() |
01e33f5412 | ||
![]() |
51fdc484c3 | ||
![]() |
3d9fa462a6 | ||
![]() |
32b5d67806 | ||
![]() |
20d3681da3 | ||
![]() |
9b97274bf6 | ||
![]() |
ede0dff030 | ||
![]() |
4cd4635fa5 | ||
![]() |
7832219749 | ||
![]() |
a8d4726caf | ||
![]() |
4b3e20c6ca | ||
![]() |
f9a53743ce | ||
![]() |
89250c0c01 | ||
![]() |
4ef944ea08 | ||
![]() |
5f58c183f4 | ||
![]() |
f71feff916 | ||
![]() |
50fb3b314b | ||
![]() |
06298562cd | ||
![]() |
89e74f3f07 | ||
![]() |
da96c27893 | ||
![]() |
3321dd4ca7 | ||
![]() |
7106d56b33 | ||
![]() |
25cd8a9d9f | ||
![]() |
a4bb0e04ab | ||
![]() |
1df60056b2 | ||
![]() |
c4bc1f627f | ||
![]() |
f4df5852fb | ||
![]() |
152b665f2e | ||
![]() |
f1d49aaeb1 | ||
![]() |
5db293ce01 | ||
![]() |
744cda3974 |
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"]
|
||||
}
|
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v2.1.12
|
||||
uses: relative-ci/agent-action@v2.1.13
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
4
.github/workflows/release.yaml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
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
@@ -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"));
|
||||
@@ -106,6 +102,14 @@ function copyMapPanel(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
function copyZXingWasm(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
|
||||
staticPath("js")
|
||||
);
|
||||
}
|
||||
|
||||
gulp.task("copy-locale-data", async () => {
|
||||
const staticDir = paths.app_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
@@ -143,6 +147,7 @@ gulp.task("copy-static-app", async () => {
|
||||
copyMapPanel(staticDir);
|
||||
|
||||
// Qr Scanner assets
|
||||
copyZXingWasm(staticDir);
|
||||
copyQrScannerWorker(staticDir);
|
||||
});
|
||||
|
||||
|
@@ -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 };
|
@@ -1,16 +0,0 @@
|
||||
import { html } from "lit";
|
||||
import type { DemoConfig } from "../types";
|
||||
|
||||
export const demoLovelaceDescription: DemoConfig["description"] = (
|
||||
localize
|
||||
) => html`
|
||||
<p>
|
||||
${localize("ui.panel.page-demo.config.sections.description", {
|
||||
blog_post: html`<a
|
||||
href="https://www.home-assistant.io/blog/2024/03/04/dashboard-chapter-1/"
|
||||
target="_blank"
|
||||
>${localize("ui.panel.page-demo.config.sections.description_blog_post")}
|
||||
</a>`,
|
||||
})}
|
||||
</p>
|
||||
`;
|
@@ -1,5 +1,4 @@
|
||||
import type { DemoConfig } from "../types";
|
||||
import { demoLovelaceDescription } from "./description";
|
||||
import { demoEntitiesSections } from "./entities";
|
||||
import { demoLovelaceSections } from "./lovelace";
|
||||
|
||||
@@ -7,7 +6,6 @@ export const demoSections: DemoConfig = {
|
||||
authorName: "Home Assistant",
|
||||
authorUrl: "https://github.com/home-assistant/frontend/",
|
||||
name: "Home Demo",
|
||||
description: demoLovelaceDescription,
|
||||
lovelace: demoLovelaceSections,
|
||||
entities: demoEntitiesSections,
|
||||
theme: () => ({}),
|
||||
|
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
@@ -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 };
|
@@ -223,7 +223,10 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
Current version: ${this.addon.version}
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.current_version",
|
||||
{ version: this.addon.version }
|
||||
)}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link"
|
||||
>${this.supervisor.localize(
|
||||
|
@@ -38,15 +38,15 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
.label=${this.supervisor.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
<error-log-card
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.header=${this.addon.name}
|
||||
.provider=${this.addon.slug}
|
||||
show
|
||||
.filter=${this._filter}
|
||||
>
|
||||
</error-log-card>
|
||||
|
@@ -374,6 +374,9 @@ export class HassioBackups extends LitElement {
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@@ -120,9 +120,6 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
</div>
|
||||
<div class="delete" slot="end">
|
||||
<ha-icon-button
|
||||
.label=${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.remove"
|
||||
)}
|
||||
.disabled=${usedRepositories.includes(repo.slug)}
|
||||
.slug=${repo.slug}
|
||||
.path=${usedRepositories.includes(repo.slug)
|
||||
@@ -146,7 +143,11 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)
|
||||
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
|
||||
: html`<ha-md-list-item
|
||||
>${this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.no_repositories"
|
||||
)}</ha-md-list-item
|
||||
>`}
|
||||
</ha-md-list>
|
||||
<div class="layout horizontal bottom">
|
||||
<ha-textfield
|
||||
@@ -212,6 +213,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
}
|
||||
ha-md-list-item {
|
||||
position: relative;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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",
|
||||
],
|
||||
|
82
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.1",
|
||||
"@codemirror/autocomplete": "6.18.3",
|
||||
"@codemirror/commands": "6.7.1",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@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,11 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.5",
|
||||
"barcode-detector": "2.3.1",
|
||||
"chart.js": "4.4.6",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.38.1",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.39.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
@@ -114,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",
|
||||
@@ -142,35 +143,30 @@
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.1.0",
|
||||
"workbox-core": "7.1.0",
|
||||
"workbox-expiration": "7.1.0",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"workbox-routing": "7.1.0",
|
||||
"workbox-strategies": "7.1.0",
|
||||
"workbox-cacheable-response": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-expiration": "7.3.0",
|
||||
"workbox-precaching": "7.3.0",
|
||||
"workbox-routing": "7.3.0",
|
||||
"workbox-strategies": "7.3.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"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",
|
||||
@@ -190,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",
|
||||
@@ -224,15 +219,11 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.12",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.7.3",
|
||||
"mocha": "10.8.2",
|
||||
"object-hash": "3.0.0",
|
||||
"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",
|
||||
@@ -241,12 +232,12 @@
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.6.3",
|
||||
"webpack": "5.95.0",
|
||||
"webpack": "5.96.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"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",
|
||||
@@ -256,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"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.7 KiB |
BIN
public/static/images/voice-assistant/area.png
Normal file
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 5.9 KiB |
BIN
public/static/images/voice-assistant/change-wake-word.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 5.8 KiB |
BIN
public/static/images/voice-assistant/error.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
public/static/images/voice-assistant/great-job.png
Normal file
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 6.4 KiB |
BIN
public/static/images/voice-assistant/heart.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 5.9 KiB |
BIN
public/static/images/voice-assistant/hi.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 5.9 KiB |
BIN
public/static/images/voice-assistant/ok-nabu.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 5.3 KiB |
BIN
public/static/images/voice-assistant/sleep.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 6.0 KiB |
BIN
public/static/images/voice-assistant/update.png
Normal file
After Width: | Height: | Size: 21 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241030.0"
|
||||
version = "20241106.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -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
@@ -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;
|
||||
|
@@ -1185,6 +1185,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.group-header {
|
||||
padding-top: 12px;
|
||||
height: var(--data-table-row-height, 52px);
|
||||
padding-left: 12px;
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: initial;
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
query,
|
||||
state as litState,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
interface State {
|
||||
bold: boolean;
|
||||
@@ -26,12 +27,15 @@ interface State {
|
||||
export class HaAnsiToHtml extends LitElement {
|
||||
@property() public content!: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "wrap-disabled" }) public wrapDisabled =
|
||||
false;
|
||||
|
||||
@query("pre") private _pre?: HTMLPreElement;
|
||||
|
||||
@litState() private _filter = "";
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`<pre></pre>`;
|
||||
return html`<pre class=${classMap({ wrap: !this.wrapDisabled })}></pre>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
@@ -47,9 +51,11 @@ export class HaAnsiToHtml extends LitElement {
|
||||
return css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
pre.wrap {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
type PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
@@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-hls-player";
|
||||
import "./ha-web-rtc-player";
|
||||
|
||||
const MJPEG_STREAM = "mjpeg";
|
||||
|
||||
type Stream = {
|
||||
type: StreamType | typeof MJPEG_STREAM;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
export class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -46,8 +55,6 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
@state() private _capabilities?: CameraCapabilities;
|
||||
|
||||
@state() private _streamType?: StreamType;
|
||||
|
||||
@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
@@ -55,7 +62,6 @@ export class HaCameraStream extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
changedProps.has("stateObj") &&
|
||||
!this._shouldRenderMJPEG &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id
|
||||
@@ -79,50 +85,63 @@ export class HaCameraStream extends LitElement {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
if (__DEMO__ || this._shouldRenderMJPEG) {
|
||||
const streams = this._streams(
|
||||
this._capabilities?.frontend_stream_types,
|
||||
this._hlsStreams,
|
||||
this._webRtcStreams
|
||||
);
|
||||
return html`${repeat(
|
||||
streams,
|
||||
(stream) => stream.type + this.stateObj!.entity_id,
|
||||
(stream) => this._renderStream(stream)
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderStream(stream: Stream) {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
if (stream.type === MJPEG_STREAM) {
|
||||
return html`<img
|
||||
.src=${__DEMO__
|
||||
? this.stateObj.attributes.entity_picture!
|
||||
: this._connected
|
||||
? computeMJPEGStreamUrl(this.stateObj)
|
||||
: ""}
|
||||
: this._posterUrl || ""}
|
||||
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
|
||||
/>`;
|
||||
}
|
||||
return html`${this._streamType === STREAM_TYPE_HLS ||
|
||||
(!this._streamType &&
|
||||
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
|
||||
? html`<ha-hls-player
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleHlsStreams}
|
||||
class=${!this._streamType && this._webRtcStreams ? "hidden" : ""}
|
||||
></ha-hls-player>`
|
||||
: nothing}
|
||||
${this._streamType === STREAM_TYPE_WEB_RTC ||
|
||||
(!this._streamType &&
|
||||
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
|
||||
? html`<ha-web-rtc-player
|
||||
autoplay
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleWebRtcStreams}
|
||||
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
|
||||
!this._webRtcStreams
|
||||
? "hidden"
|
||||
: ""}
|
||||
></ha-web-rtc-player>`
|
||||
: nothing}`;
|
||||
|
||||
if (stream.type === STREAM_TYPE_HLS) {
|
||||
return html`<ha-hls-player
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleHlsStreams}
|
||||
class=${stream.visible ? "" : "hidden"}
|
||||
></ha-hls-player>`;
|
||||
}
|
||||
|
||||
if (stream.type === STREAM_TYPE_WEB_RTC) {
|
||||
return html`<ha-web-rtc-player
|
||||
autoplay
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleWebRtcStreams}
|
||||
class=${stream.visible ? "" : "hidden"}
|
||||
></ha-web-rtc-player>`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private async _getCapabilities() {
|
||||
@@ -130,35 +149,13 @@ export class HaCameraStream extends LitElement {
|
||||
this._hlsStreams = undefined;
|
||||
this._webRtcStreams = undefined;
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
this._capabilities = { frontend_stream_types: [] };
|
||||
return;
|
||||
}
|
||||
this._capabilities = await fetchCameraCapabilities(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
if (this._capabilities.frontend_stream_types.length === 1) {
|
||||
this._streamType = this._capabilities.frontend_stream_types[0];
|
||||
}
|
||||
}
|
||||
|
||||
private get _shouldRenderMJPEG() {
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
// Steaming is not supported by the camera so fallback to MJPEG stream
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this._capabilities &&
|
||||
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
|
||||
this._hlsStreams?.hasVideo === false) &&
|
||||
(!this._capabilities.frontend_stream_types.includes(
|
||||
STREAM_TYPE_WEB_RTC
|
||||
) ||
|
||||
this._webRtcStreams?.hasVideo === false)
|
||||
) {
|
||||
// No video in HLS stream and no video in WebRTC stream
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _getPosterUrl(): Promise<void> {
|
||||
@@ -177,28 +174,87 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
private _handleHlsStreams(ev: CustomEvent) {
|
||||
this._hlsStreams = ev.detail;
|
||||
this._pickStreamType();
|
||||
}
|
||||
|
||||
private _handleWebRtcStreams(ev: CustomEvent) {
|
||||
this._webRtcStreams = ev.detail;
|
||||
this._pickStreamType();
|
||||
}
|
||||
|
||||
private _pickStreamType() {
|
||||
if (!this._hlsStreams || !this._webRtcStreams) {
|
||||
return;
|
||||
private _streams = memoizeOne(
|
||||
(
|
||||
supportedTypes?: StreamType[],
|
||||
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
|
||||
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
|
||||
): Stream[] => {
|
||||
if (__DEMO__) {
|
||||
return [{ type: MJPEG_STREAM, visible: true }];
|
||||
}
|
||||
if (!supportedTypes) {
|
||||
return [];
|
||||
}
|
||||
if (supportedTypes.length === 0) {
|
||||
// doesn't support any stream type, fallback to mjpeg
|
||||
return [{ type: MJPEG_STREAM, visible: true }];
|
||||
}
|
||||
if (supportedTypes.length === 1) {
|
||||
// only 1 stream type, no need to choose
|
||||
if (
|
||||
(supportedTypes[0] === STREAM_TYPE_HLS &&
|
||||
hlsStreams?.hasVideo === false) ||
|
||||
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
|
||||
webRtcStreams?.hasVideo === false)
|
||||
) {
|
||||
// stream failed to load, fallback to mjpeg
|
||||
return [{ type: MJPEG_STREAM, visible: true }];
|
||||
}
|
||||
return [{ type: supportedTypes[0], visible: true }];
|
||||
}
|
||||
if (hlsStreams && webRtcStreams) {
|
||||
// fully loaded
|
||||
if (
|
||||
hlsStreams.hasVideo &&
|
||||
hlsStreams.hasAudio &&
|
||||
!webRtcStreams.hasAudio
|
||||
) {
|
||||
// webRTC stream is missing audio, use HLS
|
||||
return [{ type: STREAM_TYPE_HLS, visible: true }];
|
||||
}
|
||||
if (webRtcStreams.hasVideo) {
|
||||
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
|
||||
}
|
||||
// both streams failed to load, fallback to mjpeg
|
||||
return [{ type: MJPEG_STREAM, visible: true }];
|
||||
}
|
||||
|
||||
if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
|
||||
// one of the two streams is loaded, or errored
|
||||
// choose the one that has video or is still loading
|
||||
if (hlsStreams?.hasVideo) {
|
||||
return [
|
||||
{ type: STREAM_TYPE_HLS, visible: true },
|
||||
{ type: STREAM_TYPE_WEB_RTC, visible: false },
|
||||
];
|
||||
}
|
||||
if (hlsStreams?.hasVideo === false) {
|
||||
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
|
||||
}
|
||||
if (webRtcStreams?.hasVideo) {
|
||||
return [
|
||||
{ type: STREAM_TYPE_WEB_RTC, visible: true },
|
||||
{ type: STREAM_TYPE_HLS, visible: false },
|
||||
];
|
||||
}
|
||||
if (webRtcStreams?.hasVideo === false) {
|
||||
return [{ type: STREAM_TYPE_HLS, visible: true }];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{ type: STREAM_TYPE_HLS, visible: true },
|
||||
{ type: STREAM_TYPE_WEB_RTC, visible: false },
|
||||
];
|
||||
}
|
||||
if (
|
||||
this._hlsStreams.hasVideo &&
|
||||
this._hlsStreams.hasAudio &&
|
||||
!this._webRtcStreams.hasAudio
|
||||
) {
|
||||
this._streamType = STREAM_TYPE_HLS;
|
||||
} else if (this._webRtcStreams.hasVideo) {
|
||||
this._streamType = STREAM_TYPE_WEB_RTC;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ class HaDurationInput extends LitElement {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.clearable=${!this.required && this.data !== undefined}
|
||||
.autoValidate=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
errorMessage="Required"
|
||||
@@ -67,50 +68,79 @@ class HaDurationInput extends LitElement {
|
||||
}
|
||||
|
||||
private get _days() {
|
||||
return this.data?.days ? Number(this.data.days) : 0;
|
||||
return this.data?.days
|
||||
? Number(this.data.days)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _hours() {
|
||||
return this.data?.hours ? Number(this.data.hours) : 0;
|
||||
return this.data?.hours
|
||||
? Number(this.data.hours)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _minutes() {
|
||||
return this.data?.minutes ? Number(this.data.minutes) : 0;
|
||||
return this.data?.minutes
|
||||
? Number(this.data.minutes)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _seconds() {
|
||||
return this.data?.seconds ? Number(this.data.seconds) : 0;
|
||||
return this.data?.seconds
|
||||
? Number(this.data.seconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private get _milliseconds() {
|
||||
return this.data?.milliseconds ? Number(this.data.milliseconds) : 0;
|
||||
return this.data?.milliseconds
|
||||
? Number(this.data.milliseconds)
|
||||
: this.required || this.data
|
||||
? 0
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) {
|
||||
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
ev.stopPropagation();
|
||||
const value = { ...ev.detail.value };
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
if (!this.enableMillisecond && !value.milliseconds) {
|
||||
// @ts-ignore
|
||||
delete value.milliseconds;
|
||||
} else if (value.milliseconds > 999) {
|
||||
value.seconds += Math.floor(value.milliseconds / 1000);
|
||||
value.milliseconds %= 1000;
|
||||
}
|
||||
if (value) {
|
||||
value.hours ||= 0;
|
||||
value.minutes ||= 0;
|
||||
value.seconds ||= 0;
|
||||
|
||||
if (value.seconds > 59) {
|
||||
value.minutes += Math.floor(value.seconds / 60);
|
||||
value.seconds %= 60;
|
||||
}
|
||||
if ("days" in value) value.days ||= 0;
|
||||
if ("milliseconds" in value) value.milliseconds ||= 0;
|
||||
|
||||
if (value.minutes > 59) {
|
||||
value.hours += Math.floor(value.minutes / 60);
|
||||
value.minutes %= 60;
|
||||
}
|
||||
if (!this.enableMillisecond && !value.milliseconds) {
|
||||
// @ts-ignore
|
||||
delete value.milliseconds;
|
||||
} else if (value.milliseconds > 999) {
|
||||
value.seconds += Math.floor(value.milliseconds / 1000);
|
||||
value.milliseconds %= 1000;
|
||||
}
|
||||
|
||||
if (this.enableDay && value.hours > 24) {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
if (value.seconds > 59) {
|
||||
value.minutes += Math.floor(value.seconds / 60);
|
||||
value.seconds %= 60;
|
||||
}
|
||||
|
||||
if (value.minutes > 59) {
|
||||
value.hours += Math.floor(value.minutes / 60);
|
||||
value.minutes %= 60;
|
||||
}
|
||||
|
||||
if (this.enableDay && value.hours > 24) {
|
||||
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
|
||||
value.hours %= 24;
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -25,7 +25,6 @@ export class HaBadge extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
@@ -36,11 +35,10 @@ export class HaBadge extends LitElement {
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
color: var(--ha-heading-badge-text-color, var(--secondary-text-color));
|
||||
font-size: var(--ha-heading-badge-font-size, 14px);
|
||||
font-weight: var(--ha-heading-badge-font-weight, 400);
|
||||
line-height: var(--ha-heading-badge-line-height, 20px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import { customIcons } from "../data/custom_icons";
|
||||
import type { Chunks, Icons } from "../data/iconsets";
|
||||
import {
|
||||
MDI_PREFIXES,
|
||||
checkCacheVersion,
|
||||
findIconChunk,
|
||||
getIcon,
|
||||
writeCache,
|
||||
@@ -26,11 +25,6 @@ const mdiDeprecatedIcons: DeprecatedIcon = {};
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
// Supervisor doesn't use icons, and should not update/downgrade the icon DB.
|
||||
if (!__SUPERVISOR__) {
|
||||
checkCacheVersion();
|
||||
}
|
||||
|
||||
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
|
||||
|
||||
const cachedIcons: Record<string, string> = {};
|
||||
|
@@ -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,15 +178,19 @@ export class HaMdDialog extends MdDialog {
|
||||
}
|
||||
}
|
||||
|
||||
:host ::slotted(ha-dialog-header) {
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow: var(--dialog-content-overflow, auto);
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; // overlay navigation
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -15,6 +15,9 @@ export class HaMdListItem extends MdListItem {
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
md-item {
|
||||
overflow: var(--md-item-overflow, hidden);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
26
src/components/ha-md-select-option.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { MdSelectOption } from "@material/web/select/select-option";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select-option")
|
||||
export class HaMdSelectOption extends MdSelectOption {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select-option": HaMdSelectOption;
|
||||
}
|
||||
}
|
32
src/components/ha-md-select.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MdFilledSelect } from "@material/web/select/filled-select";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select")
|
||||
export class HaMdSelect extends MdFilledSelect {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
|
||||
--md-sys-color-surface-container-highest: var(--input-fill-color);
|
||||
--md-sys-color-on-surface: var(--input-ink-color);
|
||||
|
||||
--md-sys-color-surface-container: var(--input-fill-color);
|
||||
--md-sys-color-secondary-container: var(--input-fill-color);
|
||||
--md-menu-container-color: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select": HaMdSelect;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -4,6 +4,11 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
// The BarcodeDetector Web API is not yet supported in all browsers,
|
||||
// and "qr-scanner" defaults to a suboptimal implementation if it is not available.
|
||||
// The following import makes a better implementation available that is based on a
|
||||
// WebAssembly port of ZXing:
|
||||
import { setZXingModuleOverrides } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
@@ -16,6 +21,15 @@ import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
setZXingModuleOverrides({
|
||||
locateFile: (path: string, prefix: string) => {
|
||||
if (path.endsWith(".wasm")) {
|
||||
return "/static/js/zxing_reader.wasm";
|
||||
}
|
||||
return prefix + path;
|
||||
},
|
||||
});
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -174,7 +188,7 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
|
||||
private _qrCodeError = (err: any) => {
|
||||
if (err === "No QR code found") {
|
||||
if (err.endsWith("No QR code found")) {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._reportError(err);
|
||||
|
@@ -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,
|
||||
|
@@ -24,7 +24,7 @@ export class HaToast extends Snackbar {
|
||||
max-width: 650px;
|
||||
}
|
||||
|
||||
// Revert the default styles set by mwc-snackbar
|
||||
/* Revert the default styles set by mwc-snackbar */
|
||||
@media (max-width: 480px), (max-width: 344px) {
|
||||
.mdc-snackbar__surface {
|
||||
min-width: inherit;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -108,18 +107,18 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
console.time("WebRTC");
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
console.timeLog("WebRTC", "start clientConfig");
|
||||
this._startTimer();
|
||||
|
||||
this._logEvent("start clientConfig");
|
||||
|
||||
this._clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
console.timeLog("WebRTC", "end clientConfig", this._clientConfig);
|
||||
this._logEvent("end clientConfig", this._clientConfig);
|
||||
|
||||
this._peerConnection = new RTCPeerConnection(
|
||||
this._clientConfig.configuration
|
||||
@@ -141,11 +140,10 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._peerConnection.onsignalingstatechange = (ev) => {
|
||||
switch ((ev.target as RTCPeerConnection).signalingState) {
|
||||
case "stable":
|
||||
console.timeLog("WebRTC", "ICE negotiation complete");
|
||||
this._logEvent("ICE negotiation complete");
|
||||
break;
|
||||
default:
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
this._logEvent(
|
||||
"Signaling state changed",
|
||||
(ev.target as RTCPeerConnection).signalingState
|
||||
);
|
||||
@@ -170,7 +168,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
offerToReceiveVideo: true,
|
||||
};
|
||||
|
||||
console.timeLog("WebRTC", "start createOffer", offerOptions);
|
||||
this._logEvent("start createOffer", offerOptions);
|
||||
|
||||
const offer: RTCSessionDescriptionInit =
|
||||
await this._peerConnection.createOffer(offerOptions);
|
||||
@@ -179,9 +177,9 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
console.timeLog("WebRTC", "end createOffer", offer);
|
||||
this._logEvent("end createOffer", offer);
|
||||
|
||||
console.timeLog("WebRTC", "start setLocalDescription");
|
||||
this._logEvent("start setLocalDescription");
|
||||
|
||||
await this._peerConnection.setLocalDescription(offer);
|
||||
|
||||
@@ -189,7 +187,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
console.timeLog("WebRTC", "end setLocalDescription");
|
||||
this._logEvent("end setLocalDescription");
|
||||
|
||||
let candidates = "";
|
||||
|
||||
@@ -203,11 +201,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
resolve();
|
||||
}
|
||||
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"Ice gathering state changed",
|
||||
iceGatheringState
|
||||
);
|
||||
this._logEvent("Ice gathering state changed", iceGatheringState);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -225,7 +219,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
const offer_sdp = offer.sdp! + candidates;
|
||||
|
||||
console.timeLog("WebRTC", "start webRtcOffer", offer_sdp);
|
||||
this._logEvent("start webRtcOffer", offer_sdp);
|
||||
|
||||
try {
|
||||
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
|
||||
@@ -238,8 +232,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
};
|
||||
|
||||
private _iceConnectionStateChanged = () => {
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
this._logEvent(
|
||||
"ice connection state change",
|
||||
this._peerConnection?.iceConnectionState
|
||||
);
|
||||
@@ -265,18 +258,19 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._candidatesList = [];
|
||||
}
|
||||
if (event.type === "answer") {
|
||||
console.timeLog("WebRTC", "answer", event.answer);
|
||||
this._logEvent("answer", event.answer);
|
||||
|
||||
this._handleAnswer(event);
|
||||
}
|
||||
if (event.type === "candidate") {
|
||||
console.timeLog("WebRTC", "remote ice candidate", event.candidate);
|
||||
this._logEvent("remote ice candidate", event.candidate);
|
||||
|
||||
try {
|
||||
await this._peerConnection?.addIceCandidate(
|
||||
new RTCIceCandidate({ candidate: event.candidate, sdpMid: "0" })
|
||||
);
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
@@ -291,11 +285,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
console.timeLog(
|
||||
"WebRTC",
|
||||
"local ice candidate",
|
||||
event.candidate?.candidate
|
||||
);
|
||||
this._logEvent("local ice candidate", event.candidate?.candidate);
|
||||
|
||||
if (this._sessionId) {
|
||||
addWebRtcCandidate(
|
||||
@@ -334,19 +324,16 @@ class HaWebRtcPlayer extends LitElement {
|
||||
sdp: event.answer,
|
||||
});
|
||||
try {
|
||||
console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
|
||||
this._logEvent("start setRemoteDescription", remoteDesc);
|
||||
await this._peerConnection.setRemoteDescription(remoteDesc);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to connect WebRTC stream: " + err.message;
|
||||
this._cleanUp();
|
||||
}
|
||||
console.timeLog("WebRTC", "end setRemoteDescription");
|
||||
this._logEvent("end setRemoteDescription");
|
||||
}
|
||||
|
||||
private _cleanUp() {
|
||||
console.timeLog("WebRTC", "stopped");
|
||||
console.timeEnd("WebRTC");
|
||||
|
||||
if (this._remoteStream) {
|
||||
this._remoteStream.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
@@ -372,6 +359,9 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._peerConnection.onsignalingstatechange = null;
|
||||
|
||||
this._peerConnection = undefined;
|
||||
|
||||
this._logEvent("stopped");
|
||||
this._stopTimer();
|
||||
}
|
||||
this._unsub?.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
@@ -380,17 +370,43 @@ class HaWebRtcPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _loadedData() {
|
||||
console.timeLog("WebRTC", "loadedData");
|
||||
console.timeEnd("WebRTC");
|
||||
|
||||
const video = this._videoEl;
|
||||
const stream = video.srcObject as MediaStream;
|
||||
|
||||
fireEvent(this, "load");
|
||||
fireEvent(this, "streams", {
|
||||
const data = {
|
||||
hasAudio: Boolean(stream?.getAudioTracks().length),
|
||||
hasVideo: Boolean(stream?.getVideoTracks().length),
|
||||
});
|
||||
};
|
||||
|
||||
fireEvent(this, "load");
|
||||
fireEvent(this, "streams", data);
|
||||
|
||||
this._logEvent("loadedData", data);
|
||||
this._stopTimer();
|
||||
}
|
||||
|
||||
private _startTimer() {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.time("WebRTC");
|
||||
}
|
||||
|
||||
private _stopTimer() {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.timeEnd("WebRTC");
|
||||
}
|
||||
|
||||
private _logEvent(msg: string, ...args: unknown[]) {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.timeLog("WebRTC", msg, ...args);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -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() {
|
||||
|
@@ -86,6 +86,8 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private _mapZones: Array<Marker | Circle> = [];
|
||||
|
||||
private _mapFocusZones: Array<Marker | Circle> = [];
|
||||
|
||||
private _mapPaths: Array<Polyline | CircleMarker> = [];
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -201,7 +203,11 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._mapFocusItems.length && !this.layers?.length) {
|
||||
if (
|
||||
!this._mapFocusItems.length &&
|
||||
!this._mapFocusZones.length &&
|
||||
!this.layers?.length
|
||||
) {
|
||||
this.leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
@@ -218,13 +224,9 @@ export class HaMap extends ReactiveElement {
|
||||
: []
|
||||
);
|
||||
|
||||
if (this.fitZones) {
|
||||
this._mapZones?.forEach((zone) => {
|
||||
bounds.extend(
|
||||
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
|
||||
);
|
||||
});
|
||||
}
|
||||
this._mapFocusZones?.forEach((zone) => {
|
||||
bounds.extend("getBounds" in zone ? zone.getBounds() : zone.getLatLng());
|
||||
});
|
||||
|
||||
this.layers?.forEach((layer: any) => {
|
||||
bounds.extend(
|
||||
@@ -395,6 +397,7 @@ export class HaMap extends ReactiveElement {
|
||||
if (this._mapZones.length) {
|
||||
this._mapZones.forEach((marker) => marker.remove());
|
||||
this._mapZones = [];
|
||||
this._mapFocusZones = [];
|
||||
}
|
||||
|
||||
if (!this.entities) {
|
||||
@@ -466,13 +469,18 @@ export class HaMap extends ReactiveElement {
|
||||
);
|
||||
|
||||
// create circle around it
|
||||
this._mapZones.push(
|
||||
Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: passive ? passiveZoneColor : zoneColor,
|
||||
radius,
|
||||
})
|
||||
);
|
||||
const circle = Leaflet.circle([latitude, longitude], {
|
||||
interactive: false,
|
||||
color: passive ? passiveZoneColor : zoneColor,
|
||||
radius,
|
||||
});
|
||||
this._mapZones.push(circle);
|
||||
if (
|
||||
this.fitZones &&
|
||||
(typeof entity === "string" || entity.focus !== false)
|
||||
) {
|
||||
this._mapFocusZones.push(circle);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@@ -26,7 +26,10 @@ export class HaTraceLogbook extends LitElement {
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
No Logbook entries found for this step.
|
||||
|
@@ -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}
|
||||
`;
|
||||
})
|
||||
);
|
||||
@@ -291,7 +322,10 @@ export class HaTracePathDetails extends LitElement {
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
|
@@ -28,7 +28,10 @@ export class HaTraceTimeline extends LitElement {
|
||||
allowPick
|
||||
>
|
||||
</hat-trace-timeline>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,22 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("hat-logbook-note")
|
||||
class HatLogbookNote extends LitElement {
|
||||
@property() public domain = "automation";
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public domain: "automation" | "script" = "automation";
|
||||
|
||||
render() {
|
||||
return html`
|
||||
Not all shown logbook entries might be related to this ${this.domain}.
|
||||
`;
|
||||
if (this.domain === "script") {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_script_note"
|
||||
);
|
||||
}
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_automation_note"
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@@ -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];
|
||||
};
|
||||
|