mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-27 04:52:56 +00:00
Compare commits
270 Commits
20250228.0
...
view_popup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c86219358 | ||
|
|
868f24eb9f | ||
|
|
e271989cee | ||
|
|
ca223f9d73 | ||
|
|
8fb1cf35ad | ||
|
|
9f59be492e | ||
|
|
8429d114a8 | ||
|
|
4fbc155f8b | ||
|
|
cd39e2d0f2 | ||
|
|
a23f57256c | ||
|
|
c279efaa99 | ||
|
|
c4389ec119 | ||
|
|
50d632f8d4 | ||
|
|
dba2fba828 | ||
|
|
3890afddb9 | ||
|
|
76f187ee2c | ||
|
|
488b54cf19 | ||
|
|
29d2c29af3 | ||
|
|
a2f9101a9f | ||
|
|
7893eba7a7 | ||
|
|
94ced8af32 | ||
|
|
c4b5882b2d | ||
|
|
6e8bac2e58 | ||
|
|
8a2ab2eab4 | ||
|
|
c7e5be185d | ||
|
|
e98721aa76 | ||
|
|
4c8d661c63 | ||
|
|
b7c60ffc74 | ||
|
|
db6c728cd6 | ||
|
|
34f8335a9d | ||
|
|
ecf5068bd0 | ||
|
|
0a2a2b8a70 | ||
|
|
52f4fe6bc0 | ||
|
|
a781bca94b | ||
|
|
63b44c25f8 | ||
|
|
b96319703a | ||
|
|
9e686190f6 | ||
|
|
5ca7b1d508 | ||
|
|
7c1d74c6c3 | ||
|
|
d257f667c1 | ||
|
|
842a064682 | ||
|
|
3d8e146582 | ||
|
|
78e8bd4305 | ||
|
|
0152a79bd5 | ||
|
|
f5bb72f067 | ||
|
|
9ca6a886f5 | ||
|
|
f39011f8f4 | ||
|
|
8b190867e3 | ||
|
|
321b15a270 | ||
|
|
6ba235d540 | ||
|
|
e34fd8161c | ||
|
|
084cda8218 | ||
|
|
f06a0fa34c | ||
|
|
750c59399b | ||
|
|
a6a17cd70c | ||
|
|
de1c6a5178 | ||
|
|
04c3cd7d68 | ||
|
|
17ef74d680 | ||
|
|
098c6a2567 | ||
|
|
899288ae43 | ||
|
|
a9823f30e3 | ||
|
|
97966805fa | ||
|
|
615b228827 | ||
|
|
1819c04c27 | ||
|
|
05e303d771 | ||
|
|
53bb8251fa | ||
|
|
f6467a35db | ||
|
|
7cc6397324 | ||
|
|
f6e3e312bb | ||
|
|
be1e1ff9fc | ||
|
|
2717e1e6cb | ||
|
|
2e9f72867f | ||
|
|
ff6b318fc9 | ||
|
|
386b8ba747 | ||
|
|
e27b97abc0 | ||
|
|
772a2658cb | ||
|
|
1a076061da | ||
|
|
1cb71ed379 | ||
|
|
fb11c21518 | ||
|
|
9cfcd21a93 | ||
|
|
e3f5e921d6 | ||
|
|
df4e81be75 | ||
|
|
1519e1b90c | ||
|
|
4c8b7a30f4 | ||
|
|
1e513281f4 | ||
|
|
3c28764264 | ||
|
|
82be98dad6 | ||
|
|
3320cf1880 | ||
|
|
f7cb83482a | ||
|
|
a9ddaf1bd7 | ||
|
|
eb7923fa49 | ||
|
|
2ae70e9b54 | ||
|
|
3857c53b7f | ||
|
|
620fb6375e | ||
|
|
e18f853f7e | ||
|
|
bbe549fa86 | ||
|
|
586a137037 | ||
|
|
60010c82bd | ||
|
|
d77f962087 | ||
|
|
4c952c191a | ||
|
|
e0fbd3cd1f | ||
|
|
9f05f4df50 | ||
|
|
6fbc7b2efe | ||
|
|
8dab7c598e | ||
|
|
b24f185d62 | ||
|
|
dc5bb899d2 | ||
|
|
420477e416 | ||
|
|
cd9faf7d67 | ||
|
|
852207a5f5 | ||
|
|
1f705c07b2 | ||
|
|
39ee84b54e | ||
|
|
de402e7c1a | ||
|
|
9b74cdebc2 | ||
|
|
ebe8e54046 | ||
|
|
2bac7455cc | ||
|
|
3cbeef070a | ||
|
|
bae0c232be | ||
|
|
e65b5ae91e | ||
|
|
4a166b6c23 | ||
|
|
fe17bb89eb | ||
|
|
888b2472df | ||
|
|
ef7f499364 | ||
|
|
5d9a53dcd5 | ||
|
|
7ce166e40f | ||
|
|
2c0c48106d | ||
|
|
0e8be25a60 | ||
|
|
27379c98df | ||
|
|
4076e5655a | ||
|
|
858b8b90d8 | ||
|
|
d61c771e35 | ||
|
|
ddbf57d541 | ||
|
|
a5b7bb8391 | ||
|
|
5803ab68c2 | ||
|
|
64b9104199 | ||
|
|
7aaea37db7 | ||
|
|
3c11323ea4 | ||
|
|
4f7d5053ec | ||
|
|
7009482057 | ||
|
|
d1090e8ad3 | ||
|
|
06b969f6b6 | ||
|
|
d8b6de2afd | ||
|
|
dfd5e80436 | ||
|
|
9d4df46d5f | ||
|
|
5f4cb9e3c1 | ||
|
|
96e6169b8d | ||
|
|
0906d7aa5a | ||
|
|
02fce1f40a | ||
|
|
05aa55bfb9 | ||
|
|
d4717f1293 | ||
|
|
f6a7e40d4a | ||
|
|
d73e677bea | ||
|
|
356b74607a | ||
|
|
8cb248223d | ||
|
|
24eed2e5fa | ||
|
|
c8a21a7a2f | ||
|
|
54cc096b1a | ||
|
|
49b1198cb7 | ||
|
|
91e9836423 | ||
|
|
6aa2a576b3 | ||
|
|
9712f04662 | ||
|
|
731a9a2e07 | ||
|
|
d42bd36a3e | ||
|
|
28c355812c | ||
|
|
e09dbb474b | ||
|
|
ee10f9080d | ||
|
|
4f9ec622bf | ||
|
|
dba269f2a3 | ||
|
|
46c9af75fd | ||
|
|
df934cfed9 | ||
|
|
e871dc8151 | ||
|
|
69026cbecf | ||
|
|
dda7de3301 | ||
|
|
1e000d2740 | ||
|
|
1d747c0901 | ||
|
|
8ef769559f | ||
|
|
e141b4dbee | ||
|
|
d0545fe827 | ||
|
|
4ef3a25479 | ||
|
|
616c1fda81 | ||
|
|
e8805be561 | ||
|
|
1b6d2ac08e | ||
|
|
de4a8a0a72 | ||
|
|
58dd778b3d | ||
|
|
a4cdb294b1 | ||
|
|
07c4296771 | ||
|
|
95a99c7857 | ||
|
|
fee215fe96 | ||
|
|
5e9341bf4e | ||
|
|
58b7c76b90 | ||
|
|
6ed26407be | ||
|
|
1ac0092d4e | ||
|
|
a4e8ea366a | ||
|
|
fe70355dde | ||
|
|
01f07f6476 | ||
|
|
9ef72e4afc | ||
|
|
022ef982ca | ||
|
|
f132a32fd4 | ||
|
|
782df0473c | ||
|
|
690cd47945 | ||
|
|
8fe55b9bc0 | ||
|
|
913fdfd0eb | ||
|
|
c97916bea4 | ||
|
|
cdfe4b53bf | ||
|
|
75edc5132b | ||
|
|
c79164992b | ||
|
|
68960ba03d | ||
|
|
c3a60a9c3f | ||
|
|
4783720aaa | ||
|
|
d55b806ce5 | ||
|
|
e2ff8ce302 | ||
|
|
b26bc1dcf0 | ||
|
|
76b03d3a40 | ||
|
|
c581d6d028 | ||
|
|
d899711a48 | ||
|
|
b7be74e722 | ||
|
|
e53961d395 | ||
|
|
03b08fefb7 | ||
|
|
79374f6052 | ||
|
|
ba19849182 | ||
|
|
48338e0886 | ||
|
|
3b87fc84a9 | ||
|
|
61effc3f70 | ||
|
|
e5b460c259 | ||
|
|
7a56731f56 | ||
|
|
bfe20d3760 | ||
|
|
5a37087231 | ||
|
|
dbe5bffe22 | ||
|
|
62da09d045 | ||
|
|
75d7676b36 | ||
|
|
0bea89db91 | ||
|
|
7ad759dd95 | ||
|
|
5377c9e75d | ||
|
|
bf206aa12b | ||
|
|
73669e27f4 | ||
|
|
1b5f4d3432 | ||
|
|
49379b49d0 | ||
|
|
2827421c9f | ||
|
|
1a5a183410 | ||
|
|
88a1de9aaf | ||
|
|
4ad64ce2c8 | ||
|
|
3f1ca32d13 | ||
|
|
a6f7ee6b28 | ||
|
|
0294198fba | ||
|
|
ed6659ad8f | ||
|
|
f1d04e5178 | ||
|
|
807e87fce0 | ||
|
|
cafab61727 | ||
|
|
88e6906b6b | ||
|
|
ebc1259e39 | ||
|
|
c1f2e6d82b | ||
|
|
ef964fd23e | ||
|
|
13105c2d6f | ||
|
|
5b9262487d | ||
|
|
4ec6e324f8 | ||
|
|
20f385e053 | ||
|
|
dd441f882b | ||
|
|
3a1d371b0b | ||
|
|
8848911b34 | ||
|
|
51193cf441 | ||
|
|
a5b7f2466e | ||
|
|
3d9bde548d | ||
|
|
dfa98a4ba8 | ||
|
|
20fe5b1b71 | ||
|
|
9250ecb16f | ||
|
|
e21d1399ea | ||
|
|
5dded38ccc | ||
|
|
54cc8f025c | ||
|
|
0d215e65cd | ||
|
|
f9824a3b3b | ||
|
|
197c9219bd |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.1
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.6.1
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
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.14
|
||||
uses: relative-ci/agent-action@v2.2.0
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.11.0
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.2.0
|
||||
uses: actions/setup-node@v4.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.6.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.8.1.cjs
|
||||
|
||||
@@ -18,7 +18,7 @@ module.exports.sourceMapURL = () => {
|
||||
module.exports.ignorePackages = () => [];
|
||||
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||
module.exports.emptyPackages = ({ isHassioBuild }) =>
|
||||
[
|
||||
// Contains all color definitions for all material color sets.
|
||||
// We don't use it
|
||||
@@ -28,12 +28,6 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||
require.resolve("@polymer/font-roboto/roboto.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Compatibility not needed for latest builds
|
||||
latestBuild &&
|
||||
// wrapped in require.resolve so it blows up if file no longer exists
|
||||
require.resolve(
|
||||
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
|
||||
),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
@@ -55,7 +49,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__STATIC_PATH__: "/static/",
|
||||
__HASS_URL__: `\`${
|
||||
"HASS_URL" in process.env
|
||||
? process.env["HASS_URL"]
|
||||
? process.env.HASS_URL
|
||||
: "${location.protocol}//${location.host}"
|
||||
}\``,
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
|
||||
@@ -56,6 +56,7 @@ const getCommonTemplateVars = () => {
|
||||
);
|
||||
return {
|
||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
||||
hassUrl: process.env.HASS_URL || "",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -59,6 +59,11 @@ function copyPolyfills(staticDir) {
|
||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||
staticPath("polyfills/")
|
||||
);
|
||||
// Lit polyfill support
|
||||
fs.copySync(
|
||||
npmPath("lit/polyfill-support.js"),
|
||||
path.join(staticPath("polyfills/"), "lit-polyfill-support.js")
|
||||
);
|
||||
|
||||
// dialog-polyfill css
|
||||
copyFileDir(
|
||||
|
||||
@@ -40,20 +40,17 @@ class CustomJSON extends Transform {
|
||||
this._reviver = reviver;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
async _transform(file, _, callback) {
|
||||
try {
|
||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
||||
if (this._func) obj = this._func(obj, file.path);
|
||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
||||
const outFile = file.clone({ contents: false });
|
||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
||||
outFile.dirname += `/${dir}`;
|
||||
this.push(outFile);
|
||||
}
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
||||
if (this._func) obj = this._func(obj, file.path);
|
||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
||||
const outFile = file.clone({ contents: false });
|
||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
||||
outFile.dirname += `/${dir}`;
|
||||
this.push(outFile);
|
||||
}
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,25 +65,19 @@ class MergeJSON extends Transform {
|
||||
this._reviver = reviver;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
async _transform(file, _, callback) {
|
||||
try {
|
||||
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
|
||||
if (!this._outFile) this._outFile = file.clone({ contents: false });
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
|
||||
if (!this._outFile) this._outFile = file.clone({ contents: false });
|
||||
callback(null);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
async _flush(callback) {
|
||||
try {
|
||||
const mergedObj = merge(this._startObj, ...this._objects);
|
||||
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
|
||||
this._outFile.stem = this._stem;
|
||||
callback(null, this._outFile);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
const mergedObj = merge(this._startObj, ...this._objects);
|
||||
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
|
||||
this._outFile.stem = this._stem;
|
||||
callback(null, this._outFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { StatsWriterPlugin } = require("webpack-stats-plugin");
|
||||
const filterStats = require("@bundle-stats/plugin-webpack-filter").default;
|
||||
const filterStats = require("@bundle-stats/plugin-webpack-filter");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebpackBar = require("webpackbar/rspack");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
@@ -155,9 +160,7 @@ const createRspackConfig = ({
|
||||
},
|
||||
}),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle.emptyPackages({ latestBuild, isHassioBuild }).join("|")
|
||||
),
|
||||
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
@@ -192,6 +195,7 @@ const createRspackConfig = ({
|
||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||
"lit/directives/guard$": "lit/directives/guard.js",
|
||||
"lit/directives/cache$": "lit/directives/cache.js",
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
|
||||
@@ -309,7 +309,7 @@ export class HcMain extends HassElement {
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
rawConfig.strategy,
|
||||
rawConfig,
|
||||
this.hass!
|
||||
);
|
||||
this._handleNewLovelaceConfig(config);
|
||||
@@ -351,10 +351,7 @@ export class HcMain extends HassElement {
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceDashboardStrategy(
|
||||
DEFAULT_CONFIG.strategy,
|
||||
this.hass!
|
||||
)
|
||||
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import "../../../src/components/ha-spinner";
|
||||
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import type {
|
||||
@@ -44,9 +44,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
<div class="picker">
|
||||
<div class="label">
|
||||
${this._switching
|
||||
? html`
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
`
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: until(
|
||||
selectedDemoConfig.then(
|
||||
(conf) => html`
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { validateConfig } from "../../../src/data/config";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("validate_config", () => ({
|
||||
actions: { valid: true },
|
||||
conditions: { valid: true },
|
||||
triggers: { valid: true },
|
||||
hass.mockWS<typeof validateConfig>("validate_config", () => ({
|
||||
actions: { valid: true, error: null },
|
||||
conditions: { valid: true, error: null },
|
||||
triggers: { valid: true, error: null },
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import type { getConfigEntries } from "../../../src/data/config_entries";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("config_entries/get", () => ({
|
||||
entry_id: "co2signal",
|
||||
domain: "co2signal",
|
||||
title: "Electricity Maps",
|
||||
source: "user",
|
||||
state: "loaded",
|
||||
supports_options: false,
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
}));
|
||||
hass.mockWS<typeof getConfigEntries>("config_entries/get", () => [
|
||||
{
|
||||
entry_id: "mock-entry-co2signal",
|
||||
domain: "co2signal",
|
||||
title: "Electricity Maps",
|
||||
source: "user",
|
||||
state: "loaded",
|
||||
supports_options: false,
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
supports_reconfigure: true,
|
||||
supported_subentry_types: {},
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
num_subentries: 0,
|
||||
error_reason_translation_key: null,
|
||||
error_reason_translation_placeholders: null,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-bar";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "@material/web/progress/circular-progress";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
@customElement("demo-components-ha-circular-progress")
|
||||
export class DemoHaCircularProgress extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<ha-card header="Basic circular progress">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress></div
|
||||
></ha-card>
|
||||
<ha-card header="Different circular progress sizes">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="tiny"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="medium"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="large"
|
||||
></ha-circular-progress></div
|
||||
></ha-card>
|
||||
<ha-card header="Circular progress with an aria-label">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
aria-label="Doing something..."
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
.ariaLabel=${"Doing something..."}
|
||||
></ha-circular-progress></div
|
||||
></ha-card>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-circular-progress": DemoHaCircularProgress;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiPacMan } from "@mdi/js";
|
||||
import { mdiLightbulbOn, mdiPacMan } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
@@ -125,6 +125,23 @@ const SAMPLES: {
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
template(slot, leftChevron) {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
slot=${slot}
|
||||
.leftChevron=${leftChevron}
|
||||
header="Attr Header with actions"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
.path=${mdiLightbulbOn}
|
||||
></ha-svg-icon>
|
||||
${SHORT_TEXT}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-expansion-panel")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: Circular Progress
|
||||
title: Spinner
|
||||
subtitle: Can be used to indicate an ongoing task.
|
||||
---
|
||||
44
gallery/src/pages/components/ha-spinner.ts
Normal file
44
gallery/src/pages/components/ha-spinner.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-bar";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
@customElement("demo-components-ha-spinner")
|
||||
export class DemoHaSpinner extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<ha-card header="Basic spinner">
|
||||
<div class="card-content">
|
||||
<ha-spinner></ha-spinner></div
|
||||
></ha-card>
|
||||
<ha-card header="Different spinner sizes">
|
||||
<div class="card-content">
|
||||
<ha-spinner size="tiny"></ha-spinner>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
<ha-spinner size="medium"></ha-spinner>
|
||||
<ha-spinner size="large"></ha-spinner></div
|
||||
></ha-card>
|
||||
<ha-card header="Spinner with an aria-label">
|
||||
<div class="card-content">
|
||||
<ha-spinner aria-label="Doing something..."></ha-spinner>
|
||||
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner></div
|
||||
></ha-card>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-spinner": DemoHaSpinner;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
@@ -21,7 +21,7 @@ class HassioAddonConfigDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
const hasConfiguration =
|
||||
(this.addon.options && Object.keys(this.addon.options).length) ||
|
||||
|
||||
@@ -113,8 +113,9 @@ class HassioAddonConfig extends LitElement {
|
||||
required: entry.required,
|
||||
selector: {
|
||||
text: {
|
||||
type:
|
||||
entry.format || MASKED_FIELDS.includes(entry.name)
|
||||
type: entry.format
|
||||
? entry.format
|
||||
: MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import "../../../../src/components/ha-card";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-markdown";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
@@ -33,7 +33,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
|
||||
@@ -11,7 +11,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import type { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
fetchAddonInfo,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
@@ -23,7 +23,7 @@ class HassioAddonInfoDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-spinner></ha-spinner>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1331,6 +1331,12 @@ class HassioAddonInfo extends LitElement {
|
||||
ha-alert mwc-button {
|
||||
--mdc-theme-primary: var(--primary-text-color);
|
||||
}
|
||||
|
||||
:host > ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import type { HassioAddonDetails } from "../../../../src/data/hassio/addon";
|
||||
import type { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import { haStyle } from "../../../../src/resources/styles";
|
||||
@@ -28,9 +28,7 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
`;
|
||||
return html` <ha-spinner></ha-spinner> `;
|
||||
}
|
||||
return html`
|
||||
<div class="search">
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import type { HassioBackup } from "../../../src/data/hassio/backup";
|
||||
import { uploadBackup } from "../../../src/data/hassio/backup";
|
||||
|
||||
@@ -12,6 +12,7 @@ import "../../../../src/components/ha-md-dialog";
|
||||
import "../../../../src/components/ha-dialog-header";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-button-menu";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
@@ -138,7 +139,7 @@ class HassioBackupDialog
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._restoringBackup
|
||||
? html`<div class="loading">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`
|
||||
: html`
|
||||
<supervisor-backup-content
|
||||
@@ -310,10 +311,6 @@ class HassioBackupDialog
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import {
|
||||
createHassioFullBackup,
|
||||
@@ -58,7 +59,7 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
)}
|
||||
>
|
||||
${this._creatingBackup
|
||||
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
@@ -142,10 +143,6 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-select";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import {
|
||||
@@ -69,12 +69,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
?hideActions=${this.moving}
|
||||
>
|
||||
${this.moving
|
||||
? html` <ha-circular-progress
|
||||
aria-label="Moving"
|
||||
size="large"
|
||||
indeterminate
|
||||
>
|
||||
</ha-circular-progress>
|
||||
? html`<ha-spinner aria-label="Moving" size="large"></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.moving_desc"
|
||||
@@ -166,7 +161,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
ha-select {
|
||||
width: 100%;
|
||||
}
|
||||
ha-circular-progress {
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
@@ -161,12 +161,8 @@ export class DialogHassioNetwork
|
||||
.disabled=${this._scanning}
|
||||
>
|
||||
${this._scanning
|
||||
? html`<ha-circular-progress
|
||||
aria-label="Scanning"
|
||||
indeterminate
|
||||
size="small"
|
||||
>
|
||||
</ha-circular-progress>`
|
||||
? html`<ha-spinner aria-label="Scanning" size="small">
|
||||
</ha-spinner>`
|
||||
: this.supervisor.localize("dialog.network.scan_ap")}
|
||||
</mwc-button>
|
||||
${this._accessPoints &&
|
||||
@@ -282,8 +278,7 @@ export class DialogHassioNetwork
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress indeterminate size="small">
|
||||
</ha-circular-progress>`
|
||||
? html`<ha-spinner size="small"> </ha-spinner>`
|
||||
: this.supervisor.localize("common.save")}
|
||||
</mwc-button>
|
||||
</div>`;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "../../../../src/components/ha-spinner";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import type {
|
||||
@@ -161,10 +161,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
></ha-textfield>
|
||||
<mwc-button @click=${this._addRepository}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
? html`<ha-spinner size="small"></ha-spinner>`
|
||||
: this._dialogParams!.supervisor.localize(
|
||||
"dialog.repositories.add"
|
||||
)}
|
||||
@@ -202,7 +199,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
ha-circular-progress {
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import "./hassio-main";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
|
||||
@@ -15,6 +15,7 @@ import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-spinner";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
@@ -192,12 +193,10 @@ class UpdateAvailableCard extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: html`<ha-circular-progress
|
||||
: html`<ha-spinner
|
||||
aria-label="Updating"
|
||||
size="large"
|
||||
indeterminate
|
||||
>
|
||||
</ha-circular-progress>
|
||||
></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
@@ -465,7 +464,7 @@ class UpdateAvailableCard extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { fileDownload } from "../../../src/util/file_download";
|
||||
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
|
||||
import { waitForSeconds } from "../../../src/common/util/wait";
|
||||
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
|
||||
|
||||
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
|
||||
declare global {
|
||||
@@ -216,7 +218,7 @@ class LandingPageLogs extends LitElement {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
// fallback to observerlogs if there is a problem with supervisor
|
||||
// fallback to observer logs if there is a problem with supervisor
|
||||
this._loadObserverLogs();
|
||||
}
|
||||
}
|
||||
@@ -251,6 +253,9 @@ class LandingPageLogs extends LitElement {
|
||||
|
||||
this._scheduleObserverLogs();
|
||||
} catch (err) {
|
||||
// wait because there is a moment where landingpage is down and core is not up yet
|
||||
await waitForSeconds(ASSUME_CORE_START_SECONDS);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._error = true;
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
type CSSResultGroup,
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
LocalizeFunc,
|
||||
@@ -16,34 +10,24 @@ import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
setSupervisorNetworkDns,
|
||||
type NetworkInfo,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
import type { NetworkInterface } from "../../../src/data/hassio/network";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("landing-page-network")
|
||||
class LandingPageNetwork extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
@property({ attribute: false }) public networkInfo?: NetworkInfo;
|
||||
|
||||
@state() private _getNetworkInfoError = false;
|
||||
|
||||
@state() private _dnsPrimaryInterfaceNameservers?: string;
|
||||
|
||||
@state() private _dnsPrimaryInterface?: string;
|
||||
@property({ type: Boolean }) public error = false;
|
||||
|
||||
protected render() {
|
||||
if (!this._networkIssue && !this._getNetworkInfoError) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._getNetworkInfoError) {
|
||||
if (this.error) {
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
<p>${this.localize("network_issue.error_get_network_info")}</p>
|
||||
@@ -51,6 +35,16 @@ class LandingPageNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
let dnsPrimaryInterfaceNameservers: string | undefined;
|
||||
|
||||
const primaryInterface = this._getPrimaryInterface(
|
||||
this.networkInfo?.interfaces
|
||||
);
|
||||
if (primaryInterface) {
|
||||
dnsPrimaryInterfaceNameservers =
|
||||
this._getPrimaryNameservers(primaryInterface);
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
@@ -58,11 +52,11 @@ class LandingPageNetwork extends LitElement {
|
||||
>
|
||||
<p>
|
||||
${this.localize("network_issue.description", {
|
||||
dns: this._dnsPrimaryInterfaceNameservers || "?",
|
||||
dns: dnsPrimaryInterfaceNameservers || "?",
|
||||
})}
|
||||
</p>
|
||||
<p>${this.localize("network_issue.resolve_different")}</p>
|
||||
${!this._dnsPrimaryInterfaceNameservers
|
||||
${!dnsPrimaryInterfaceNameservers
|
||||
? html`
|
||||
<p>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
@@ -74,7 +68,7 @@ class LandingPageNetwork extends LitElement {
|
||||
({ translationKey }, key) =>
|
||||
html`<ha-button
|
||||
.index=${key}
|
||||
.disabled=${!this._dnsPrimaryInterfaceNameservers}
|
||||
.disabled=${!dnsPrimaryInterfaceNameservers}
|
||||
@click=${this._setDns}
|
||||
>${this.localize(translationKey)}</ha-button
|
||||
>`
|
||||
@@ -84,97 +78,40 @@ class LandingPageNetwork extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this._pingSupervisor();
|
||||
}
|
||||
private _getPrimaryInterface = memoizeOne((interfaces?: NetworkInterface[]) =>
|
||||
interfaces?.find((intf) => intf.primary && intf.enabled)
|
||||
);
|
||||
|
||||
private _schedulePingSupervisor() {
|
||||
setTimeout(
|
||||
() => this._pingSupervisor(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _pingSupervisor() {
|
||||
try {
|
||||
const response = await pingSupervisor();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to ping supervisor, assume update in progress");
|
||||
}
|
||||
this._fetchSupervisorInfo();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._schedulePingSupervisor();
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
setTimeout(
|
||||
() => this._fetchSupervisorInfo(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchSupervisorInfo() {
|
||||
let data;
|
||||
try {
|
||||
const response = await getSupervisorNetworkInfo();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch network info");
|
||||
}
|
||||
|
||||
({ data } = await response.json());
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._getNetworkInfoError = true;
|
||||
this._dnsPrimaryInterfaceNameservers = undefined;
|
||||
this._dnsPrimaryInterface = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._getNetworkInfoError = false;
|
||||
|
||||
const primaryInterface = data.interfaces.find(
|
||||
(intf) => intf.primary && intf.enabled
|
||||
);
|
||||
if (primaryInterface) {
|
||||
this._dnsPrimaryInterfaceNameservers = [
|
||||
private _getPrimaryNameservers = memoizeOne(
|
||||
(primaryInterface: NetworkInterface) =>
|
||||
[
|
||||
...(primaryInterface.ipv4?.nameservers || []),
|
||||
...(primaryInterface.ipv6?.nameservers || []),
|
||||
].join(", ");
|
||||
|
||||
this._dnsPrimaryInterface = primaryInterface.interface;
|
||||
} else {
|
||||
this._dnsPrimaryInterfaceNameservers = undefined;
|
||||
this._dnsPrimaryInterface = undefined;
|
||||
}
|
||||
|
||||
if (!data.host_internet) {
|
||||
this._networkIssue = true;
|
||||
} else {
|
||||
this._networkIssue = false;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._networkIssue,
|
||||
});
|
||||
this._scheduleFetchSupervisorInfo();
|
||||
}
|
||||
].join(", ")
|
||||
);
|
||||
|
||||
private async _setDns(ev) {
|
||||
const primaryInterface = this._getPrimaryInterface(
|
||||
this.networkInfo?.interfaces
|
||||
);
|
||||
|
||||
const index = ev.target?.index;
|
||||
try {
|
||||
const dnsPrimaryInterface = primaryInterface?.interface;
|
||||
if (!dnsPrimaryInterface) {
|
||||
throw new Error("No primary interface found");
|
||||
}
|
||||
|
||||
const response = await setSupervisorNetworkDns(
|
||||
index,
|
||||
this._dnsPrimaryInterface!
|
||||
dnsPrimaryInterface
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to set DNS");
|
||||
}
|
||||
this._networkIssue = false;
|
||||
|
||||
// notify landing page to trigger a network info reload
|
||||
fireEvent(this, "dns-set");
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
@@ -205,4 +142,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"landing-page-network": LandingPageNetwork;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"dns-set": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import type { LandingPageKeys } from "../../../src/common/translations/localize";
|
||||
import type { HassioResponse } from "../../../src/data/hassio/common";
|
||||
import type {
|
||||
DockerNetwork,
|
||||
NetworkInterface,
|
||||
} from "../../../src/data/hassio/network";
|
||||
import { handleFetchPromise } from "../../../src/util/hass-call-api";
|
||||
|
||||
export interface NetworkInfo {
|
||||
interfaces: NetworkInterface[];
|
||||
docker: DockerNetwork;
|
||||
host_internet: boolean;
|
||||
supervisor_internet: boolean;
|
||||
}
|
||||
|
||||
export const ALTERNATIVE_DNS_SERVERS: {
|
||||
ipv4: string[];
|
||||
@@ -37,8 +50,11 @@ export async function pingSupervisor() {
|
||||
return fetch("/supervisor-api/supervisor/ping");
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo() {
|
||||
return fetch("/supervisor-api/network/info");
|
||||
export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
|
||||
const responseData = await handleFetchPromise<HassioResponse<NetworkInfo>>(
|
||||
fetch("/supervisor-api/network/info")
|
||||
);
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
|
||||
@@ -10,36 +10,56 @@ import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
import {
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
} from "./data/supervisor";
|
||||
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 5;
|
||||
export const ASSUME_CORE_START_SECONDS = 60;
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@property({ attribute: false }) public translationFragment = "landing-page";
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
|
||||
@state() private _supervisorError = false;
|
||||
|
||||
@state() private _networkInfo?: NetworkInfo;
|
||||
|
||||
@state() private _coreStatusChecked = false;
|
||||
|
||||
@state() private _networkInfoError = false;
|
||||
|
||||
@state() private _coreCheckActive = false;
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
render() {
|
||||
const networkIssue = this._networkInfo && !this._networkInfo.host_internet;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h1>${this.localize("header")}</h1>
|
||||
${!this._networkIssue && !this._supervisorError
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<mwc-linear-progress indeterminate></mwc-linear-progress>
|
||||
`
|
||||
: nothing}
|
||||
<landing-page-network
|
||||
@value-changed=${this._networkInfoChanged}
|
||||
.localize=${this.localize}
|
||||
></landing-page-network>
|
||||
|
||||
${networkIssue || this._networkInfoError
|
||||
? html`
|
||||
<landing-page-network
|
||||
.localize=${this.localize}
|
||||
.networkInfo=${this._networkInfo}
|
||||
.error=${this._networkInfoError}
|
||||
@dns-set=${this._fetchSupervisorInfo}
|
||||
></landing-page-network>
|
||||
`
|
||||
: nothing}
|
||||
${this._supervisorError
|
||||
? html`
|
||||
<ha-alert
|
||||
@@ -88,24 +108,66 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
}
|
||||
import("../../src/components/ha-language-picker");
|
||||
|
||||
this._scheduleCoreCheck();
|
||||
this._fetchSupervisorInfo(true);
|
||||
}
|
||||
|
||||
private _scheduleCoreCheck() {
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
setTimeout(
|
||||
() => this._checkCoreAvailability(),
|
||||
SCHEDULE_CORE_CHECK_SECONDS * 1000
|
||||
() => this._fetchSupervisorInfo(true),
|
||||
// on assumed core start check every second, otherwise every 5 seconds
|
||||
(this._coreCheckActive
|
||||
? SCHEDULE_CORE_CHECK_SECONDS
|
||||
: SCHEDULE_FETCH_NETWORK_INFO_SECONDS) * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleTurnOffCoreCheck() {
|
||||
setTimeout(() => {
|
||||
this._coreCheckActive = false;
|
||||
}, ASSUME_CORE_START_SECONDS * 1000);
|
||||
}
|
||||
|
||||
private async _fetchSupervisorInfo(schedule = false) {
|
||||
try {
|
||||
const response = await pingSupervisor();
|
||||
if (!response.ok) {
|
||||
throw new Error("ping-failed");
|
||||
}
|
||||
|
||||
this._networkInfo = await getSupervisorNetworkInfo();
|
||||
this._networkInfoError = false;
|
||||
this._coreStatusChecked = false;
|
||||
} catch (err: any) {
|
||||
if (!this._coreStatusChecked) {
|
||||
// wait before show errors, because we assume that core is starting
|
||||
this._coreCheckActive = true;
|
||||
this._scheduleTurnOffCoreCheck();
|
||||
}
|
||||
await this._checkCoreAvailability();
|
||||
|
||||
// assume supervisor update if ping fails -> don't show an error
|
||||
if (!this._coreCheckActive && err.message !== "ping-failed") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._networkInfoError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule) {
|
||||
this._scheduleFetchSupervisorInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkCoreAvailability() {
|
||||
try {
|
||||
const response = await fetch("/manifest.json");
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error("Failed to fetch manifest");
|
||||
}
|
||||
} finally {
|
||||
this._scheduleCoreCheck();
|
||||
} catch (_err) {
|
||||
this._coreStatusChecked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +175,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
this._supervisorError = true;
|
||||
}
|
||||
|
||||
private _networkInfoChanged(ev: CustomEvent) {
|
||||
this._networkIssue = ev.detail.value;
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
const language = ev.detail.value;
|
||||
if (language !== this.language && language) {
|
||||
|
||||
106
package.json
106
package.json
@@ -26,25 +26,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.9",
|
||||
"@babel/runtime": "7.27.0",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.0",
|
||||
"@codemirror/language": "6.10.8",
|
||||
"@codemirror/legacy-modes": "6.4.3",
|
||||
"@codemirror/search": "6.5.9",
|
||||
"@codemirror/language": "6.11.0",
|
||||
"@codemirror/legacy-modes": "6.5.0",
|
||||
"@codemirror/search": "6.5.10",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.36.3",
|
||||
"@codemirror/view": "6.36.5",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.17.3",
|
||||
"@formatjs/intl-displaynames": "6.8.10",
|
||||
"@formatjs/intl-durationformat": "0.7.3",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.4",
|
||||
"@formatjs/intl-listformat": "7.7.10",
|
||||
"@formatjs/intl-locale": "4.2.10",
|
||||
"@formatjs/intl-numberformat": "8.15.3",
|
||||
"@formatjs/intl-pluralrules": "5.4.3",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.10",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
"@formatjs/intl-durationformat": "0.7.4",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.5",
|
||||
"@formatjs/intl-listformat": "7.7.11",
|
||||
"@formatjs/intl-locale": "4.2.11",
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.2.0",
|
||||
"@material/web": "2.3.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
@@ -89,19 +89,21 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.2",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.0",
|
||||
"@shoelace-style/shoelace": "2.20.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.6.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.6.5",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.0",
|
||||
"barcode-detector": "3.0.1",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.40.0",
|
||||
"core-js": "3.41.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
@@ -109,21 +111,21 @@
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"element-internals-polyfill": "1.3.13",
|
||||
"element-internals-polyfill": "3.0.1",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.7.15",
|
||||
"intl-messageformat": "10.7.16",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "2.8.0",
|
||||
"lit-html": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"luxon": "3.6.0",
|
||||
"marked": "15.0.7",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
@@ -137,9 +139,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "2.0.2",
|
||||
"ua-parser-js": "2.0.3",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.16",
|
||||
@@ -154,20 +154,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.9",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.3",
|
||||
"@babel/core": "7.26.10",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||
"@babel/plugin-proposal-decorators": "7.25.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.9",
|
||||
"@babel/plugin-transform-runtime": "7.26.10",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.18.2",
|
||||
"@lokalise/node-api": "13.2.1",
|
||||
"@octokit/auth-oauth-device": "7.1.3",
|
||||
"@octokit/plugin-retry": "7.1.4",
|
||||
"@babel/preset-typescript": "7.27.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.19.1",
|
||||
"@lokalise/node-api": "14.2.0",
|
||||
"@octokit/auth-oauth-device": "7.1.4",
|
||||
"@octokit/plugin-retry": "7.2.0",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@rsdoctor/rspack-plugin": "0.4.13",
|
||||
"@rspack/cli": "1.2.5",
|
||||
"@rspack/core": "1.2.5",
|
||||
"@rsdoctor/rspack-plugin": "1.0.1",
|
||||
"@rspack/cli": "1.2.8",
|
||||
"@rspack/core": "1.2.8",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.21",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -175,7 +175,7 @@
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/leaflet": "1.9.17",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
@@ -186,20 +186,20 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "3.0.6",
|
||||
"babel-loader": "9.2.1",
|
||||
"@vitest/coverage-v8": "3.0.9",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.21.0",
|
||||
"eslint": "9.23.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit": "2.0.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.1",
|
||||
"eslint-plugin-wc": "3.0.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"glob": "11.0.1",
|
||||
@@ -211,23 +211,23 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.4.3",
|
||||
"lint-staged": "15.5.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.5.2",
|
||||
"prettier": "3.5.3",
|
||||
"rspack-manifest-plugin": "5.0.3",
|
||||
"serve": "14.2.4",
|
||||
"sinon": "19.0.2",
|
||||
"sinon": "20.0.0",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.11",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.24.1",
|
||||
"typescript": "5.8.2",
|
||||
"typescript-eslint": "8.28.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.0.6",
|
||||
"vitest": "3.0.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
@@ -244,5 +244,5 @@
|
||||
"globals": "16.0.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0"
|
||||
"packageManager": "yarn@4.8.1"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=75.1"]
|
||||
requires = ["setuptools~=77.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250228.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
version = "20250326.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -17,8 +18,6 @@ requires-python = ">=3.13.0"
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
[tool.setuptools]
|
||||
platforms = ["any"]
|
||||
zip-safe = false
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{
|
||||
"description": "Group tsparticles engine and presets",
|
||||
"groupName": "tsparticles",
|
||||
"matchPackageNames": ["tsparticles-engine", "tsparticles-preset-{/,}**"]
|
||||
"matchPackageNames": ["@tsparticles/engine", "@tsparticles/preset-{/,}**"]
|
||||
},
|
||||
{
|
||||
"description": "Group date-fns with dependent timezone package",
|
||||
|
||||
@@ -132,6 +132,15 @@ export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||
|
||||
export function theme2hex(themeColor: string): string {
|
||||
if (themeColor.startsWith("#")) {
|
||||
if (themeColor.length === 4 || themeColor.length === 5) {
|
||||
const c = themeColor;
|
||||
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
|
||||
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||||
}
|
||||
if (themeColor.length === 9) {
|
||||
// Ignore alpha channel.
|
||||
return themeColor.substring(0, 7);
|
||||
}
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
differenceInMilliseconds,
|
||||
differenceInMonths,
|
||||
endOfMonth,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
differenceInDays,
|
||||
addDays,
|
||||
} from "date-fns";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
@@ -100,6 +104,32 @@ export const shiftDateRange = (
|
||||
locale,
|
||||
config
|
||||
);
|
||||
} else if (
|
||||
calcDateProperty(
|
||||
startDate,
|
||||
(date) => startOfDay(date).getMilliseconds() === date.getMilliseconds(),
|
||||
locale,
|
||||
config
|
||||
) &&
|
||||
calcDateProperty(
|
||||
endDate,
|
||||
(date) => endOfDay(date).getMilliseconds() === date.getMilliseconds(),
|
||||
locale,
|
||||
config
|
||||
)
|
||||
) {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
endDate,
|
||||
startDate,
|
||||
differenceInDays,
|
||||
locale,
|
||||
config
|
||||
) as number) +
|
||||
1) *
|
||||
(forward ? 1 : -1);
|
||||
start = calcDate(startDate, addDays, locale, config, difference);
|
||||
end = calcDate(endDate, addDays, locale, config, difference);
|
||||
} else {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
|
||||
116
src/common/datetime/calc_date_range.ts
Normal file
116
src/common/datetime/calc_date_range.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
startOfQuarter,
|
||||
endOfQuarter,
|
||||
subDays,
|
||||
subMonths,
|
||||
} from "date-fns";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { calcDate } from "./calc_date";
|
||||
import { firstWeekdayIndex } from "./first_weekday";
|
||||
|
||||
export type DateRange =
|
||||
| "today"
|
||||
| "yesterday"
|
||||
| "this_week"
|
||||
| "this_month"
|
||||
| "this_quarter"
|
||||
| "this_year"
|
||||
| "now-7d"
|
||||
| "now-30d"
|
||||
| "now-12m"
|
||||
| "now-1h"
|
||||
| "now-12h"
|
||||
| "now-24h";
|
||||
|
||||
export const calcDateRange = (
|
||||
hass: HomeAssistant,
|
||||
range: DateRange
|
||||
): [Date, Date] => {
|
||||
const today = new Date();
|
||||
const weekStartsOn = firstWeekdayIndex(hass.locale);
|
||||
switch (range) {
|
||||
case "today":
|
||||
return [
|
||||
calcDate(today, startOfDay, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
calcDate(today, endOfDay, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
];
|
||||
case "yesterday":
|
||||
return [
|
||||
calcDate(addDays(today, -1), startOfDay, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
calcDate(addDays(today, -1), endOfDay, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
];
|
||||
case "this_week":
|
||||
return [
|
||||
calcDate(today, startOfWeek, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
calcDate(today, endOfWeek, hass.locale, hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
];
|
||||
case "this_month":
|
||||
return [
|
||||
calcDate(today, startOfMonth, hass.locale, hass.config),
|
||||
calcDate(today, endOfMonth, hass.locale, hass.config),
|
||||
];
|
||||
case "this_quarter":
|
||||
return [
|
||||
calcDate(today, startOfQuarter, hass.locale, hass.config),
|
||||
calcDate(today, endOfQuarter, hass.locale, hass.config),
|
||||
];
|
||||
case "this_year":
|
||||
return [
|
||||
calcDate(today, startOfYear, hass.locale, hass.config),
|
||||
calcDate(today, endOfYear, hass.locale, hass.config),
|
||||
];
|
||||
case "now-7d":
|
||||
return [
|
||||
calcDate(today, subDays, hass.locale, hass.config, 7),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 1),
|
||||
];
|
||||
case "now-30d":
|
||||
return [
|
||||
calcDate(today, subDays, hass.locale, hass.config, 30),
|
||||
calcDate(today, subDays, hass.locale, hass.config, 1),
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
|
||||
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
calcDate(today, subHours, hass.locale, hass.config, 1),
|
||||
calcDate(today, subHours, hass.locale, hass.config, 0),
|
||||
];
|
||||
case "now-12h":
|
||||
return [
|
||||
calcDate(today, subHours, hass.locale, hass.config, 12),
|
||||
calcDate(today, subHours, hass.locale, hass.config, 0),
|
||||
];
|
||||
case "now-24h":
|
||||
return [
|
||||
calcDate(today, subHours, hass.locale, hass.config, 24),
|
||||
calcDate(today, subHours, hass.locale, hass.config, 0),
|
||||
];
|
||||
}
|
||||
return [today, today];
|
||||
};
|
||||
4
src/common/entity/compute_area_name.ts
Normal file
4
src/common/entity/compute_area_name.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
@@ -34,7 +34,7 @@ export const computeAttributeValueDisplay = (
|
||||
value !== undefined ? value : stateObj.attributes[attribute];
|
||||
|
||||
// Null value, the state is unknown
|
||||
if (attributeValue === null) {
|
||||
if (attributeValue === null || attributeValue === undefined) {
|
||||
return localize("state.default.unknown");
|
||||
}
|
||||
|
||||
|
||||
38
src/common/entity/compute_device_name.ts
Normal file
38
src/common/entity/compute_device_name.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
|
||||
export const computeDeviceName = (
|
||||
device: DeviceRegistryEntry
|
||||
): string | undefined => (device.name_by_user || device.name)?.trim();
|
||||
|
||||
export const computeDeviceNameDisplay = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant,
|
||||
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) =>
|
||||
computeDeviceName(device) ||
|
||||
(entities && fallbackDeviceName(hass, entities)) ||
|
||||
hass.localize("ui.panel.config.devices.unnamed_device", {
|
||||
type: hass.localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
),
|
||||
});
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
hass: HomeAssistant,
|
||||
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) => {
|
||||
for (const entity of entities || []) {
|
||||
const entityId = typeof entity === "string" ? entity : entity.entity_id;
|
||||
const stateObj = hass.states[entityId];
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
59
src/common/entity/compute_entity_name.ts
Normal file
59
src/common/entity/compute_entity_name.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
): string | undefined => {
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
if (!entry) {
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry, hass);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): string | undefined => {
|
||||
const name =
|
||||
entry.name || ("original_name" in entry ? entry.original_name : undefined);
|
||||
|
||||
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// If the device name is the same as the entity name, consider empty entity name
|
||||
if (deviceName === name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Remove the device name from the entity name if it starts with it
|
||||
if (deviceName && name) {
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
4
src/common/entity/compute_floor_name.ts
Normal file
4
src/common/entity/compute_floor_name.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export const computeFloorName = (floor: FloorRegistryEntry): string =>
|
||||
floor.name?.trim();
|
||||
30
src/common/entity/context/get_area_context.ts
Normal file
30
src/common/entity/context/get_area_context.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
interface AreaContext {
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getAreaContext = (
|
||||
areaId: string,
|
||||
hass: HomeAssistant
|
||||
): AreaContext => {
|
||||
const area = (hass.areas[areaId] as AreaRegistryEntry | undefined) || null;
|
||||
|
||||
if (!area) {
|
||||
return {
|
||||
area: null,
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
||||
43
src/common/entity/context/get_entity_context.ts
Normal file
43
src/common/entity/context/get_entity_context.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
interface EntityContext {
|
||||
entity: EntityRegistryDisplayEntry | null;
|
||||
device: DeviceRegistryEntry | null;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getEntityContext = (
|
||||
entityId: string,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const entity =
|
||||
(hass.entities[entityId] as EntityRegistryDisplayEntry | undefined) || null;
|
||||
|
||||
if (!entity) {
|
||||
return {
|
||||
entity: null,
|
||||
device: null,
|
||||
area: null,
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const deviceId = entity?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : null;
|
||||
const areaId = entity?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
entity: entity,
|
||||
device: device,
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
||||
78
src/common/entity/entity_domain_filter.ts
Normal file
78
src/common/entity/entity_domain_filter.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export type EntityDomainFilterFunc = (entityId: string) => boolean;
|
||||
|
||||
export interface EntityDomainFilter {
|
||||
include_domains: string[];
|
||||
include_entities: string[];
|
||||
exclude_domains: string[];
|
||||
exclude_entities: string[];
|
||||
}
|
||||
|
||||
export const isEmptyEntityDomainFilter = (filter: EntityDomainFilter) =>
|
||||
filter.include_domains.length +
|
||||
filter.include_entities.length +
|
||||
filter.exclude_domains.length +
|
||||
filter.exclude_entities.length ===
|
||||
0;
|
||||
|
||||
export const generateEntityDomainFilter = (
|
||||
includeDomains?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeDomains?: string[],
|
||||
excludeEntities?: string[]
|
||||
): EntityDomainFilterFunc => {
|
||||
const includeDomainsSet = new Set(includeDomains);
|
||||
const includeEntitiesSet = new Set(includeEntities);
|
||||
const excludeDomainsSet = new Set(excludeDomains);
|
||||
const excludeEntitiesSet = new Set(excludeEntities);
|
||||
|
||||
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
|
||||
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
|
||||
|
||||
// Case 1 - no includes or excludes - pass all entities
|
||||
if (!haveInclude && !haveExclude) {
|
||||
return () => true;
|
||||
}
|
||||
|
||||
// Case 2 - includes, no excludes - only include specified entities
|
||||
if (haveInclude && !haveExclude) {
|
||||
return (entityId) =>
|
||||
includeEntitiesSet.has(entityId) ||
|
||||
includeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
|
||||
// Case 3 - excludes, no includes - only exclude specified entities
|
||||
if (!haveInclude && haveExclude) {
|
||||
return (entityId) =>
|
||||
!excludeEntitiesSet.has(entityId) &&
|
||||
!excludeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
|
||||
// Case 4 - both includes and excludes specified
|
||||
// Case 4a - include domain specified
|
||||
// - if domain is included, pass if entity not excluded
|
||||
// - if domain is not included, pass if entity is included
|
||||
// note: if both include and exclude domains specified,
|
||||
// the exclude domains are ignored
|
||||
if (includeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
includeDomainsSet.has(computeDomain(entityId))
|
||||
? !excludeEntitiesSet.has(entityId)
|
||||
: includeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4b - exclude domain specified
|
||||
// - if domain is excluded, pass if entity is included
|
||||
// - if domain is not excluded, pass if entity not excluded
|
||||
if (excludeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
excludeDomainsSet.has(computeDomain(entityId))
|
||||
? includeEntitiesSet.has(entityId)
|
||||
: !excludeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4c - neither include or exclude domain specified
|
||||
// - Only pass if entity is included. Ignore entity excludes.
|
||||
return (entityId) => includeEntitiesSet.has(entityId);
|
||||
};
|
||||
@@ -1,78 +1,121 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
export type FilterFunc = (entityId: string) => boolean;
|
||||
type EntityCategory = "none" | "config" | "diagnostic";
|
||||
|
||||
export interface EntityFilter {
|
||||
include_domains: string[];
|
||||
include_entities: string[];
|
||||
exclude_domains: string[];
|
||||
exclude_entities: string[];
|
||||
domain?: string | string[];
|
||||
device_class?: string | string[];
|
||||
device?: string | string[];
|
||||
area?: string | string[];
|
||||
floor?: string | string[];
|
||||
label?: string | string[];
|
||||
entity_category?: EntityCategory | EntityCategory[];
|
||||
hidden_platform?: string | string[];
|
||||
}
|
||||
|
||||
export const isEmptyFilter = (filter: EntityFilter) =>
|
||||
filter.include_domains.length +
|
||||
filter.include_entities.length +
|
||||
filter.exclude_domains.length +
|
||||
filter.exclude_entities.length ===
|
||||
0;
|
||||
export type EntityFilterFunc = (entityId: string) => boolean;
|
||||
|
||||
export const generateFilter = (
|
||||
includeDomains?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeDomains?: string[],
|
||||
excludeEntities?: string[]
|
||||
): FilterFunc => {
|
||||
const includeDomainsSet = new Set(includeDomains);
|
||||
const includeEntitiesSet = new Set(includeEntities);
|
||||
const excludeDomainsSet = new Set(excludeDomains);
|
||||
const excludeEntitiesSet = new Set(excludeEntities);
|
||||
export const generateEntityFilter = (
|
||||
hass: HomeAssistant,
|
||||
filter: EntityFilter
|
||||
): EntityFilterFunc => {
|
||||
const domains = filter.domain
|
||||
? new Set(ensureArray(filter.domain))
|
||||
: undefined;
|
||||
const deviceClasses = filter.device_class
|
||||
? new Set(ensureArray(filter.device_class))
|
||||
: undefined;
|
||||
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
|
||||
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
|
||||
const devices = filter.device
|
||||
? new Set(ensureArray(filter.device))
|
||||
: undefined;
|
||||
const entityCategories = filter.entity_category
|
||||
? new Set(ensureArray(filter.entity_category))
|
||||
: undefined;
|
||||
const labels = filter.label ? new Set(ensureArray(filter.label)) : undefined;
|
||||
const hiddenPlatforms = filter.hidden_platform
|
||||
? new Set(ensureArray(filter.hidden_platform))
|
||||
: undefined;
|
||||
|
||||
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
|
||||
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
|
||||
return (entityId: string) => {
|
||||
const stateObj = hass.states[entityId] as HassEntity | undefined;
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
if (domains) {
|
||||
const domain = computeDomain(entityId);
|
||||
if (!domains.has(domain)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (deviceClasses) {
|
||||
const dc = stateObj.attributes.device_class || "none";
|
||||
if (!deviceClasses.has(dc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 1 - no includes or excludes - pass all entities
|
||||
if (!haveInclude && !haveExclude) {
|
||||
return () => true;
|
||||
}
|
||||
const { area, floor, device, entity } = getEntityContext(entityId, hass);
|
||||
|
||||
// Case 2 - includes, no excludes - only include specified entities
|
||||
if (haveInclude && !haveExclude) {
|
||||
return (entityId) =>
|
||||
includeEntitiesSet.has(entityId) ||
|
||||
includeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
if (entity && entity.hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 3 - excludes, no includes - only exclude specified entities
|
||||
if (!haveInclude && haveExclude) {
|
||||
return (entityId) =>
|
||||
!excludeEntitiesSet.has(entityId) &&
|
||||
!excludeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
if (floors) {
|
||||
if (!floor) {
|
||||
return false;
|
||||
}
|
||||
if (!floors) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (areas) {
|
||||
if (!area) {
|
||||
return false;
|
||||
}
|
||||
if (!areas.has(area.area_id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (devices) {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (!devices.has(device.id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (labels) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (!entity.labels.some((label) => labels.has(label))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (entityCategories) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const category = entity?.entity_category || "none";
|
||||
if (!entityCategories.has(category)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (hiddenPlatforms) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (entity.platform && hiddenPlatforms.has(entity.platform)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 4 - both includes and excludes specified
|
||||
// Case 4a - include domain specified
|
||||
// - if domain is included, pass if entity not excluded
|
||||
// - if domain is not included, pass if entity is included
|
||||
// note: if both include and exclude domains specified,
|
||||
// the exclude domains are ignored
|
||||
if (includeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
includeDomainsSet.has(computeDomain(entityId))
|
||||
? !excludeEntitiesSet.has(entityId)
|
||||
: includeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4b - exclude domain specified
|
||||
// - if domain is excluded, pass if entity is included
|
||||
// - if domain is not excluded, pass if entity not excluded
|
||||
if (excludeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
excludeDomainsSet.has(computeDomain(entityId))
|
||||
? includeEntitiesSet.has(entityId)
|
||||
: !excludeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4c - neither include or exclude domain specified
|
||||
// - Only pass if entity is included. Ignore entity excludes.
|
||||
return (entityId) => includeEntitiesSet.has(entityId);
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
18
src/common/entity/get_area_context.ts
Normal file
18
src/common/entity/get_area_context.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface AreaContext {
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
export const getAreaContext = (
|
||||
area: AreaRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): AreaContext => {
|
||||
const floorId = area.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
floor: floor,
|
||||
};
|
||||
};
|
||||
24
src/common/entity/get_device_context.ts
Normal file
24
src/common/entity/get_device_context.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface DeviceContext {
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getDeviceContext = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): DeviceContext => {
|
||||
const areaId = device.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
||||
55
src/common/entity/get_entity_context.ts
Normal file
55
src/common/entity/get_entity_context.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
interface EntityContext {
|
||||
device: DeviceRegistryEntry | null;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getEntityContext = (
|
||||
stateObj: HassEntity,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const entry = hass.entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
|
||||
if (!entry) {
|
||||
return {
|
||||
device: null,
|
||||
area: null,
|
||||
floor: null,
|
||||
};
|
||||
}
|
||||
return getEntityEntryContext(entry, hass);
|
||||
};
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
| EntityRegistryEntry
|
||||
| ExtEntityRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): EntityContext => {
|
||||
const deviceId = entry?.device_id;
|
||||
const device = deviceId ? hass.devices[deviceId] : null;
|
||||
const areaId = entry?.area_id || device?.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : null;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : null;
|
||||
|
||||
return {
|
||||
device: device,
|
||||
area: area,
|
||||
floor: floor,
|
||||
};
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
const SUFFIXES = [" ", ": "];
|
||||
const SUFFIXES = [" ", ": ", " - "];
|
||||
|
||||
/**
|
||||
* Strips a device name from an entity name.
|
||||
* @param entityName the entity name
|
||||
* @param lowerCasedPrefix the prefix to strip, lower cased
|
||||
* @param prefix the prefix to strip
|
||||
* @returns
|
||||
*/
|
||||
export const stripPrefixFromEntityName = (
|
||||
entityName: string,
|
||||
lowerCasedPrefix: string
|
||||
prefix: string
|
||||
) => {
|
||||
const lowerCasedEntityName = entityName.toLowerCase();
|
||||
|
||||
const lowerCasedPrefix = prefix.toLowerCase();
|
||||
for (const suffix of SUFFIXES) {
|
||||
const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`;
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const webComponentsSupported =
|
||||
"customElements" in window && "content" in document.createElement("template");
|
||||
export const webComponentsSupported = "attachShadow" in Element.prototype;
|
||||
|
||||
@@ -45,3 +45,22 @@ export const caseInsensitiveStringCompare = (
|
||||
|
||||
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
|
||||
};
|
||||
|
||||
export const orderCompare = (order: string[]) => (a: string, b: string) => {
|
||||
const idxA = order.indexOf(a);
|
||||
const idxB = order.indexOf(b);
|
||||
|
||||
if (idxA === idxB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (idxA === -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (idxB === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return idxA - idxB;
|
||||
};
|
||||
|
||||
6
src/common/util/wait.ts
Normal file
6
src/common/util/wait.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const waitForMs = (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export const waitForSeconds = (seconds: number) => waitForMs(seconds * 1000);
|
||||
@@ -1,31 +1,38 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../ha-circular-progress";
|
||||
import "../ha-button";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-progress-button")
|
||||
export class HaProgressButton extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public progress = false;
|
||||
|
||||
@property({ type: Boolean }) public raised = false;
|
||||
|
||||
@property({ type: Boolean }) public unelevated = false;
|
||||
|
||||
@state() private _result?: "success" | "error";
|
||||
|
||||
public render(): TemplateResult {
|
||||
const overlay = this._result || this.progress;
|
||||
return html`
|
||||
<mwc-button
|
||||
?raised=${this.raised}
|
||||
<ha-button
|
||||
.raised=${this.raised}
|
||||
.label=${this.label}
|
||||
.unelevated=${this.unelevated}
|
||||
.disabled=${this.disabled || this.progress}
|
||||
class=${this._result || ""}
|
||||
>
|
||||
<slot name="icon" slot="icon"></slot>
|
||||
<slot></slot>
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
${!overlay
|
||||
? nothing
|
||||
: html`
|
||||
@@ -35,12 +42,7 @@ export class HaProgressButton extends LitElement {
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: this.progress
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
? html`<ha-spinner size="small"></ha-spinner>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
@@ -70,12 +72,12 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button {
|
||||
ha-button {
|
||||
transition: all 1s;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
mwc-button.success {
|
||||
ha-button.success {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--success-color);
|
||||
transition: none;
|
||||
@@ -83,12 +85,13 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].success {
|
||||
ha-button[unelevated].success,
|
||||
ha-button[raised].success {
|
||||
--mdc-theme-primary: var(--success-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
|
||||
mwc-button.error {
|
||||
ha-button.error {
|
||||
--mdc-theme-primary: white;
|
||||
background-color: var(--error-color);
|
||||
transition: none;
|
||||
@@ -96,7 +99,8 @@ export class HaProgressButton extends LitElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
mwc-button[raised].error {
|
||||
ha-button[unelevated].error,
|
||||
ha-button[raised].error {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
--mdc-theme-on-primary: white;
|
||||
}
|
||||
@@ -113,8 +117,8 @@ export class HaProgressButton extends LitElement {
|
||||
color: white;
|
||||
}
|
||||
|
||||
mwc-button.success slot,
|
||||
mwc-button.error slot {
|
||||
ha-button.success slot,
|
||||
ha-button.error slot {
|
||||
visibility: hidden;
|
||||
}
|
||||
:host([destructive]) {
|
||||
|
||||
@@ -108,7 +108,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Add keyboard event listeners
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
if (
|
||||
!this._modifierPressed &&
|
||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||
) {
|
||||
this._modifierPressed = true;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
@@ -123,7 +126,10 @@ export class HaChartBase extends LitElement {
|
||||
};
|
||||
|
||||
const handleKeyUp = (ev: KeyboardEvent) => {
|
||||
if ((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control")) {
|
||||
if (
|
||||
this._modifierPressed &&
|
||||
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
|
||||
) {
|
||||
this._modifierPressed = false;
|
||||
if (!this.options?.dataZoom) {
|
||||
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
|
||||
@@ -220,7 +226,12 @@ export class HaChartBase extends LitElement {
|
||||
const overflowLimit = isMobile
|
||||
? LEGEND_OVERFLOW_LIMIT_MOBILE
|
||||
: LEGEND_OVERFLOW_LIMIT;
|
||||
return html`<div class="chart-legend">
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
"chart-legend": true,
|
||||
"multiple-items": items.length > 1,
|
||||
})}
|
||||
>
|
||||
<ul>
|
||||
${items.map((item: string, index: number) => {
|
||||
if (!this.expandLegend && index >= overflowLimit) {
|
||||
@@ -252,9 +263,13 @@ export class HaChartBase extends LitElement {
|
||||
<ha-assist-chip
|
||||
@click=${this._toggleExpandedLegend}
|
||||
filled
|
||||
label=${`${this.hass.localize(
|
||||
`ui.components.history_charts.${this.expandLegend ? "collapse_legend" : "expand_legend"}`
|
||||
)} (${items.length})`}
|
||||
label=${this.expandLegend
|
||||
? this.hass.localize(
|
||||
"ui.components.history_charts.collapse_legend"
|
||||
)
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.expand_legend"
|
||||
)} (${items.length - overflowLimit})`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
@@ -316,6 +331,16 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._hiddenDatasets.add(stat);
|
||||
}
|
||||
});
|
||||
|
||||
this.chart.setOption({
|
||||
...this._createOptions(),
|
||||
series: this._getSeries(),
|
||||
@@ -562,8 +587,8 @@ export class HaChartBase extends LitElement {
|
||||
fontSize: 12,
|
||||
},
|
||||
axisPointer: {
|
||||
lineStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
crossStyle: { color: style.getPropertyValue("--divider-color") },
|
||||
lineStyle: { color: style.getPropertyValue("--info-color") },
|
||||
crossStyle: { color: style.getPropertyValue("--info-color") },
|
||||
},
|
||||
},
|
||||
timeline: {},
|
||||
@@ -689,7 +714,7 @@ export class HaChartBase extends LitElement {
|
||||
.chart-legend {
|
||||
max-height: 60%;
|
||||
overflow-y: auto;
|
||||
margin: 12px 0 0;
|
||||
padding: 12px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -709,10 +734,10 @@ export class HaChartBase extends LitElement {
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
box-sizing: border-box;
|
||||
max-width: 220px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chart-legend.multiple-items li {
|
||||
max-width: 220px;
|
||||
}
|
||||
.chart-legend .hidden {
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -124,7 +124,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
const data = dataset.data || [];
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const point = data[i];
|
||||
if (point && point[0] <= time && point[1]) {
|
||||
if (point && point[0] <= time && typeof point[1] === "number") {
|
||||
lastData = point;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -296,7 +296,11 @@ export class StatisticsChart extends LitElement {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
scale: true,
|
||||
scale:
|
||||
this.chartType !== "bar" ||
|
||||
this.logarithmicScale ||
|
||||
minYAxis !== undefined ||
|
||||
maxYAxis !== undefined,
|
||||
min: this._clampYAxis(minYAxis),
|
||||
max: this._clampYAxis(maxYAxis),
|
||||
splitLine: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
@@ -13,10 +14,7 @@ import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../data/device_registry";
|
||||
import {
|
||||
computeDeviceName,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../../data/device_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../../data/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
@@ -214,7 +212,7 @@ export class HaDevicePicker extends LitElement {
|
||||
}
|
||||
|
||||
const outputDevices = inputDevices.map((device) => {
|
||||
const name = computeDeviceName(
|
||||
const name = computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass,
|
||||
deviceEntityLookup[device.id]
|
||||
|
||||
@@ -371,6 +371,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.renderer=${this._rowRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
|
||||
@@ -193,17 +193,16 @@ export class StateBadge extends LitElement {
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
width: 40px;
|
||||
color: var(--paper-item-icon-color, #44739e);
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
background-size: cover;
|
||||
line-height: 40px;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
--state-inactive-color: initial;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreaFilterValue {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-area-filter")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaFilterValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const allAreasCount = Object.keys(this.hass.areas).length;
|
||||
const hiddenAreasCount = this.value?.hidden?.length ?? 0;
|
||||
|
||||
const description =
|
||||
hiddenAreasCount === 0
|
||||
? this.hass.localize("ui.components.area-filter.all_areas")
|
||||
: allAreasCount === hiddenAreasCount
|
||||
? this.hass.localize("ui.components.area-filter.no_areas")
|
||||
: this.hass.localize("ui.components.area-filter.area_count", {
|
||||
count: allAreasCount - hiddenAreasCount,
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-list-item
|
||||
tabindex="0"
|
||||
role="button"
|
||||
hasMeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
@click=${this._edit}
|
||||
@keydown=${this._edit}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<span>${this.label}</span>
|
||||
<span slot="secondary">${description}</span>
|
||||
<ha-icon-next
|
||||
slot="meta"
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _edit(ev) {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const value = await showAreaFilterDialog(this, {
|
||||
title: this.label,
|
||||
initialValue: this.value,
|
||||
});
|
||||
if (!value) return;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding-left: 8px;
|
||||
--mdc-list-side-padding-right: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-filter": HaAreaPicker;
|
||||
}
|
||||
}
|
||||
102
src/components/ha-areas-display-editor.ts
Normal file
102
src/components/ha-areas-display-editor.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areaCompare } from "../data/area_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreasDisplayValue {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-areas-display-editor")
|
||||
export class HaAreasDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreasDisplayValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-navigation-button" })
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const compare = areaCompare(this.hass.areas);
|
||||
|
||||
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
|
||||
compare(areaA.area_id, areaB.area_id)
|
||||
);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area.area_id, this.hass!);
|
||||
return {
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
icon: area.icon ?? undefined,
|
||||
iconPath: mdiTextureBox,
|
||||
description: floor?.name,
|
||||
};
|
||||
});
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
hidden: this.value?.hidden ?? [],
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.label}
|
||||
.expanded=${this.expanded}
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
.showNavigationButton=${this.showNavigationButton}
|
||||
></ha-items-display-editor>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
const newValue: AreasDisplayValue = {
|
||||
...this.value,
|
||||
...value,
|
||||
};
|
||||
if (newValue.hidden?.length === 0) {
|
||||
delete newValue.hidden;
|
||||
}
|
||||
if (newValue.order?.length === 0) {
|
||||
delete newValue.order;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-areas-display-editor": HaAreasDisplayEditor;
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,7 @@ export class HaAssistChat extends LitElement {
|
||||
this._addMessage(userMessage);
|
||||
this.requestUpdate("_audioRecorder");
|
||||
|
||||
let continueConversation = false;
|
||||
let hassMessage = {
|
||||
who: "hass",
|
||||
text: "…",
|
||||
@@ -369,6 +370,8 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
if (event.type === "intent-end") {
|
||||
this._conversationId = event.data.intent_output.conversation_id;
|
||||
continueConversation =
|
||||
event.data.intent_output.continue_conversation;
|
||||
const plain = event.data.intent_output.response.speech?.plain;
|
||||
if (plain) {
|
||||
hassMessage.text = plain.speech;
|
||||
@@ -380,7 +383,12 @@ export class HaAssistChat extends LitElement {
|
||||
const url = event.data.tts_output.url;
|
||||
this._audio = new Audio(url);
|
||||
this._audio.play();
|
||||
this._audio.addEventListener("ended", this._unloadAudio);
|
||||
this._audio.addEventListener("ended", () => {
|
||||
this._unloadAudio();
|
||||
if (continueConversation) {
|
||||
this._startListening();
|
||||
}
|
||||
});
|
||||
this._audio.addEventListener("pause", this._unloadAudio);
|
||||
this._audio.addEventListener("canplaythrough", this._playAudio);
|
||||
this._audio.addEventListener("error", this._audioError);
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { MdCircularProgress } from "@material/web/progress/circular-progress";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-circular-progress")
|
||||
export class HaCircularProgress extends MdCircularProgress {
|
||||
@property({ attribute: "aria-label", type: String }) public ariaLabel =
|
||||
"Loading";
|
||||
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("size")) {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
this.style.setProperty("--md-circular-progress-size", "16px");
|
||||
break;
|
||||
case "small":
|
||||
this.style.setProperty("--md-circular-progress-size", "28px");
|
||||
break;
|
||||
case "medium":
|
||||
this.style.setProperty("--md-circular-progress-size", "48px");
|
||||
break;
|
||||
case "large":
|
||||
this.style.setProperty("--md-circular-progress-size", "68px");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
--md-circular-progress-size: 48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-circular-progress": HaCircularProgress;
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,9 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public opened = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
||||
|
||||
@query("ha-textfield", true) private _inputElement!: HaTextField;
|
||||
@@ -187,7 +190,7 @@ export class HaComboBox extends LitElement {
|
||||
>
|
||||
<slot name="icon" slot="leadingIcon"></slot>
|
||||
</ha-textfield>
|
||||
${this.value
|
||||
${this.value && !this.hideClearIcon
|
||||
? html`<ha-svg-icon
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
@@ -204,6 +207,7 @@ export class HaComboBox extends LitElement {
|
||||
aria-expanded=${this.opened ? "true" : "false"}
|
||||
class="toggle-button"
|
||||
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._toggleOpen}
|
||||
></ha-svg-icon>
|
||||
</vaadin-combo-box-light>
|
||||
@@ -356,6 +360,10 @@ export class HaComboBox extends LitElement {
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.toggle-button[disabled] {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
top: -7px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -24,6 +25,16 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
|
||||
@property() public options;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
public override render() {
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs";
|
||||
import { DIRECTION_ALL, Manager, Pan, Press, Tap } from "@egjs/hammerjs";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -159,6 +159,7 @@ export class HaControlSlider extends LitElement {
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
this._mc.add(new Press());
|
||||
|
||||
let savedValue;
|
||||
this._mc.on("panstart", () => {
|
||||
@@ -190,7 +191,7 @@ export class HaControlSlider extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
});
|
||||
|
||||
this._mc.on("singletap", (e) => {
|
||||
this._mc.on("singletap pressup", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
this.value = this.steppedValue(this.percentageToValue(percentage));
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
DIRECTION_HORIZONTAL,
|
||||
DIRECTION_VERTICAL,
|
||||
Manager,
|
||||
Press,
|
||||
Swipe,
|
||||
Tap,
|
||||
} from "@egjs/hammerjs";
|
||||
@@ -79,6 +80,7 @@ export class HaControlSwitch extends LitElement {
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
this._mc.add(new Press());
|
||||
|
||||
if (this.vertical) {
|
||||
this._mc.on("swipeup", () => {
|
||||
@@ -106,10 +108,11 @@ export class HaControlSwitch extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
this._mc.on("singletap", () => {
|
||||
this._mc.on("singletap pressup", () => {
|
||||
if (this.disabled) return;
|
||||
this._toggle();
|
||||
});
|
||||
|
||||
this.addEventListener("keydown", this._keydown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const COUNTRIES = [
|
||||
export const COUNTRIES = [
|
||||
"AD",
|
||||
"AE",
|
||||
"AF",
|
||||
|
||||
@@ -3,25 +3,13 @@ import "@material/mwc-list/mwc-list";
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
subHours,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
startOfDay,
|
||||
startOfMonth,
|
||||
startOfWeek,
|
||||
startOfYear,
|
||||
isThisYear,
|
||||
} from "date-fns";
|
||||
import { isThisYear } from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
|
||||
import { shiftDateRange } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import {
|
||||
formatShortDateTime,
|
||||
@@ -36,9 +24,28 @@ import "./ha-icon-button";
|
||||
import "./ha-icon-button-next";
|
||||
import "./ha-icon-button-prev";
|
||||
import "./ha-textarea";
|
||||
import { calcDateRange } from "../common/datetime/calc_date_range";
|
||||
import type { DateRange } from "../common/datetime/calc_date_range";
|
||||
|
||||
export type DateRangePickerRanges = Record<string, [Date, Date]>;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"preset-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
const RANGE_KEYS: DateRange[] = ["today", "yesterday", "this_week"];
|
||||
const EXTENDED_RANGE_KEYS: DateRange[] = [
|
||||
"this_month",
|
||||
"this_year",
|
||||
"now-1h",
|
||||
"now-12h",
|
||||
"now-24h",
|
||||
"now-7d",
|
||||
"now-30d",
|
||||
];
|
||||
|
||||
@customElement("ha-date-range-picker")
|
||||
export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -84,194 +91,16 @@ export class HaDateRangePicker extends LitElement {
|
||||
(changedProps.has("hass") &&
|
||||
this.hass?.localize !== changedProps.get("hass")?.localize)
|
||||
) {
|
||||
const today = new Date();
|
||||
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||
const weekStart = calcDate(
|
||||
today,
|
||||
startOfWeek,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
);
|
||||
const weekEnd = calcDate(
|
||||
today,
|
||||
endOfWeek,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
);
|
||||
const rangeKeys = this.extendedPresets
|
||||
? [...RANGE_KEYS, ...EXTENDED_RANGE_KEYS]
|
||||
: RANGE_KEYS;
|
||||
|
||||
this._ranges = {
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
||||
calcDate(today, startOfDay, this.hass.locale, this.hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
calcDate(today, endOfDay, this.hass.locale, this.hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.yesterday"
|
||||
)]: [
|
||||
calcDate(
|
||||
addDays(today, -1),
|
||||
startOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
calcDate(
|
||||
addDays(today, -1),
|
||||
endOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.this_week"
|
||||
)]: [weekStart, weekEnd],
|
||||
...(this.extendedPresets
|
||||
? {
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.this_month"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
startOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
endOfMonth,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.this_year"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
startOfYear,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
calcDate(today, endOfYear, this.hass.locale, this.hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-1h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
1
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-12h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
12
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-24h"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-7d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 7
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.now-30d"
|
||||
)]: [
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
24 * 30
|
||||
),
|
||||
calcDate(
|
||||
today,
|
||||
subHours,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
0
|
||||
),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
this._ranges = {};
|
||||
rangeKeys.forEach((key) => {
|
||||
this._ranges![
|
||||
this.hass.localize(`ui.components.date-range-picker.ranges.${key}`)
|
||||
] = calcDateRange(this.hass, key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +239,10 @@ export class HaDateRangePicker extends LitElement {
|
||||
const dateRange = Object.values(this.ranges || this._ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
|
||||
fireEvent(this, "preset-selected", {
|
||||
index: ev.detail.index,
|
||||
});
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
dateRangePicker.clickRange(dateRange);
|
||||
dateRangePicker.clickedApply();
|
||||
|
||||
@@ -139,6 +139,7 @@ export class HaDialog extends DialogBase {
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
}
|
||||
.header_title {
|
||||
display: flex;
|
||||
|
||||
50
src/components/ha-divider.ts
Normal file
50
src/components/ha-divider.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-divider")
|
||||
export class HaMdDivider extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
role=${ifDefined(this.label ? "separator" : undefined)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
>
|
||||
<span class="line"></span>
|
||||
${this.label
|
||||
? html`
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="line"></span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
width: var(--ha-divider-width, 100%);
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.label {
|
||||
padding: var(--ha-divider-label-padding, 0 16px);
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
background-color: var(--divider-color);
|
||||
height: var(--ha-divider-line-height, 1px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
81
src/components/ha-entities-display-editor.ts
Normal file
81
src/components/ha-entities-display-editor.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { entityIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
|
||||
export interface EntitiesDisplayValue {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
}
|
||||
|
||||
@customElement("ha-entities-display-editor")
|
||||
export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: EntitiesDisplayValue;
|
||||
|
||||
@property({ attribute: false }) public entitiesIds: string[] = [];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const entities = this.entitiesIds
|
||||
.map((entityId) => this.hass.states[entityId])
|
||||
.filter(Boolean);
|
||||
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(this.hass, entity),
|
||||
}));
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
hidden: this.value?.hidden ?? [],
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._itemDisplayChanged}
|
||||
></ha-items-display-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemDisplayChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
const newValue: EntitiesDisplayValue = {
|
||||
...this.value,
|
||||
...value,
|
||||
};
|
||||
if (newValue.hidden?.length === 0) {
|
||||
delete newValue.hidden;
|
||||
}
|
||||
if (newValue.order?.length === 0) {
|
||||
delete newValue.order;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entities-display-editor": HaEntitiesDisplayEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -13,11 +13,11 @@ export class HaExpansionPanel extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@property({ attribute: false, type: Boolean, reflect: true }) leftChevron =
|
||||
false;
|
||||
@property({ attribute: "left-chevron", type: Boolean, reflect: true })
|
||||
public leftChevron = false;
|
||||
|
||||
@property({ attribute: false, type: Boolean, reflect: true }) noCollapse =
|
||||
false;
|
||||
@property({ attribute: "no-collapse", type: Boolean, reflect: true })
|
||||
public noCollapse = false;
|
||||
|
||||
@property() header?: string;
|
||||
|
||||
@@ -28,6 +28,14 @@ export class HaExpansionPanel extends LitElement {
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const chevronIcon = this.noCollapse
|
||||
? nothing
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
return html`
|
||||
<div class="top ${classMap({ expanded: this.expanded })}">
|
||||
<div
|
||||
@@ -42,28 +50,15 @@ export class HaExpansionPanel extends LitElement {
|
||||
aria-expanded=${this.expanded}
|
||||
aria-controls="sect1"
|
||||
>
|
||||
${this.leftChevron && !this.noCollapse
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${this.leftChevron ? chevronIcon : nothing}
|
||||
<slot name="leading-icon"></slot>
|
||||
<slot name="header">
|
||||
<div class="header">
|
||||
${this.header}
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
</div>
|
||||
</slot>
|
||||
${!this.leftChevron && !this.noCollapse
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${!this.leftChevron ? chevronIcon : nothing}
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,7 +172,8 @@ export class HaExpansionPanel extends LitElement {
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
:host([leftchevron]) .summary-icon {
|
||||
:host([left-chevron]) .summary-icon,
|
||||
::slotted([slot="leading-icon"]) {
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
margin-inline-start: 0;
|
||||
|
||||
19
src/components/ha-fade-in.ts
Normal file
19
src/components/ha-fade-in.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import SlAnimation from "@shoelace-style/shoelace/dist/components/animation/animation.component";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-fade-in")
|
||||
export class HaFadeIn extends SlAnimation {
|
||||
@property() public name = "fadeIn";
|
||||
|
||||
@property() public fill: FillMode = "both";
|
||||
|
||||
@property({ type: Boolean }) public play = true;
|
||||
|
||||
@property({ type: Number }) public iterations = 1;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-fade-in": HaFadeIn;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export class HaFilterBlueprints extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { computeDeviceName } from "../data/device_registry";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
@@ -46,7 +46,7 @@ export class HaFilterDevices extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
@@ -95,7 +95,7 @@ export class HaFilterDevices extends LitElement {
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
${computeDeviceNameDisplay(device, this.hass)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
@@ -142,12 +142,14 @@ export class HaFilterDevices extends LitElement {
|
||||
.filter(
|
||||
(device) =>
|
||||
!filter ||
|
||||
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
|
||||
computeDeviceNameDisplay(device, this.hass)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeDeviceName(a, this.hass),
|
||||
computeDeviceName(b, this.hass),
|
||||
computeDeviceNameDisplay(a, this.hass),
|
||||
computeDeviceNameDisplay(b, this.hass),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class HaFilterDomains extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class HaFilterEntities extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class HaFilterStates extends LitElement {
|
||||
const hasIcon = this.states.find((item) => item.icon);
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
|
||||
@@ -13,6 +13,12 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = field.description.suggested_value;
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
} else if (field.type === "expandable") {
|
||||
const expandableData = computeInitialHaFormData(field.schema);
|
||||
if (field.required || Object.keys(expandableData).length) {
|
||||
// Only add expandable data if it's required or any of its children have initial values.
|
||||
data[field.name] = expandableData;
|
||||
}
|
||||
} else if (!field.required) {
|
||||
// Do nothing.
|
||||
} else if (field.type === "boolean") {
|
||||
@@ -36,8 +42,6 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if (field.type === "expandable") {
|
||||
data[field.name] = computeInitialHaFormData(field.schema);
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
|
||||
|
||||
@@ -67,18 +67,23 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
|
||||
${this.schema.icon
|
||||
? html`
|
||||
<ha-icon slot="leading-icon" .icon=${this.schema.icon}></ha-icon>
|
||||
`
|
||||
: this.schema.iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
.path=${this.schema.iconPath}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
slot="header"
|
||||
role="heading"
|
||||
aria-level=${this.schema.headingLevel?.toString() ?? "3"}
|
||||
>
|
||||
${this.schema.icon
|
||||
? html` <ha-icon .icon=${this.schema.icon}></ha-icon> `
|
||||
: this.schema.iconPath
|
||||
? html`
|
||||
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${this.schema.title || this.computeLabel?.(this.schema)}
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -38,6 +38,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
<ha-textfield
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
|
||||
166
src/components/ha-form/ha-form-optional_actions.ts
Normal file
166
src/components/ha-form/ha-form-optional_actions.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import type {
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@customElement("ha-form-optional_actions")
|
||||
export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormOptionalActionsSchema;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public computeHelper?: (
|
||||
schema: HaFormSchema
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@state() private _displayActions?: string[];
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("data")) {
|
||||
const displayActions = this._displayActions ?? NO_ACTIONS;
|
||||
const hiddenActions = this._hiddenActions(
|
||||
this.schema.schema,
|
||||
displayActions
|
||||
);
|
||||
this._displayActions = [
|
||||
...displayActions,
|
||||
...hiddenActions.filter((name) => name in this.data),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private _hiddenActions = memoizeOne(
|
||||
(schema: readonly HaFormSchema[], displayActions: string[]): string[] =>
|
||||
schema
|
||||
.map((item) => item.name)
|
||||
.filter((name) => !displayActions.includes(name))
|
||||
);
|
||||
|
||||
private _displaySchema = memoizeOne(
|
||||
(
|
||||
schema: readonly HaFormSchema[],
|
||||
displayActions: string[]
|
||||
): HaFormSchema[] =>
|
||||
schema.filter((item) => displayActions.includes(item.name))
|
||||
);
|
||||
|
||||
public render(): TemplateResult {
|
||||
const displayActions = this._displayActions ?? NO_ACTIONS;
|
||||
|
||||
const schema = this._displaySchema(
|
||||
this.schema.schema,
|
||||
this._displayActions ?? []
|
||||
);
|
||||
|
||||
const hiddenActions = this._hiddenActions(
|
||||
this.schema.schema,
|
||||
displayActions
|
||||
);
|
||||
|
||||
const schemaMap = new Map<string, HaFormSchema>(
|
||||
this.computeLabel
|
||||
? this.schema.schema.map((item) => [item.name, item])
|
||||
: []
|
||||
);
|
||||
|
||||
return html`
|
||||
${schema.length > 0
|
||||
? html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${schema}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
: nothing}
|
||||
${hiddenActions.length > 0
|
||||
? html`
|
||||
<ha-button-menu
|
||||
@action=${this._handleAddAction}
|
||||
fixed
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger">
|
||||
${this.localize?.("ui.components.form-optional-actions.add") ||
|
||||
"Add interaction"}
|
||||
</ha-button>
|
||||
${hiddenActions.map((action) => {
|
||||
const actionSchema = schemaMap.get(action);
|
||||
return html`
|
||||
<ha-list-item>
|
||||
${this.computeLabel && actionSchema
|
||||
? this.computeLabel(actionSchema)
|
||||
: action}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAddAction(ev: CustomEvent) {
|
||||
const hiddenActions = this._hiddenActions(
|
||||
this.schema.schema,
|
||||
this._displayActions ?? NO_ACTIONS
|
||||
);
|
||||
const index = ev.detail.index;
|
||||
const action = hiddenActions[index];
|
||||
this._displayActions = [...(this._displayActions ?? []), action];
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
:host ha-form {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-form-optional_actions": HaFormOptionalActions;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user