Compare commits
202 Commits
revert-130
...
20220802.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
df4b83349e | ||
![]() |
d4232a2256 | ||
![]() |
f7e348c19b | ||
![]() |
f44fd35b90 | ||
![]() |
dac1d76bd2 | ||
![]() |
0ab823bcf5 | ||
![]() |
65e952aaeb | ||
![]() |
cfdf043444 | ||
![]() |
ecc1bf5206 | ||
![]() |
57d664d87d | ||
![]() |
cf2cd4043d | ||
![]() |
98761cab3f | ||
![]() |
4a622f9424 | ||
![]() |
61b42249ec | ||
![]() |
c27e3325d9 | ||
![]() |
08efc2fdd1 | ||
![]() |
53519ae8ab | ||
![]() |
9baeabed19 | ||
![]() |
f3229bb8a7 | ||
![]() |
86c971b76a | ||
![]() |
afc69cb270 | ||
![]() |
a379d29a6c | ||
![]() |
0769b14566 | ||
![]() |
1dc68b72da | ||
![]() |
40616b6af2 | ||
![]() |
c08be957ce | ||
![]() |
140e269697 | ||
![]() |
7c18d5aa0e | ||
![]() |
1acdc9cd6c | ||
![]() |
7501849044 | ||
![]() |
26ed13e548 | ||
![]() |
086c33d8b3 | ||
![]() |
c73677f15d | ||
![]() |
f7090583ac | ||
![]() |
68517018cc | ||
![]() |
adf3fa6a0e | ||
![]() |
b443ec0af5 | ||
![]() |
b9ae0e72b1 | ||
![]() |
63ea8e6568 | ||
![]() |
5be624f45d | ||
![]() |
38f19b6180 | ||
![]() |
c99f00ba50 | ||
![]() |
ce5776f59d | ||
![]() |
12ff70020a | ||
![]() |
5d605447a5 | ||
![]() |
6d88d46ce4 | ||
![]() |
cbe2643146 | ||
![]() |
d332b8ab14 | ||
![]() |
adfef05110 | ||
![]() |
ca6a7bfbe2 | ||
![]() |
a22f96a481 | ||
![]() |
688109524d | ||
![]() |
62dd7111ce | ||
![]() |
1267575f62 | ||
![]() |
b7da4dc68f | ||
![]() |
826474518f | ||
![]() |
a4b92fef3a | ||
![]() |
d41159591c | ||
![]() |
cb256bc386 | ||
![]() |
bd50d6a6a3 | ||
![]() |
05418fc83b | ||
![]() |
8b675cdbba | ||
![]() |
36b4909950 | ||
![]() |
157b3ba5f2 | ||
![]() |
72443b4f24 | ||
![]() |
1c7d3fe610 | ||
![]() |
6ac4560b36 | ||
![]() |
9309a4c7bc | ||
![]() |
b582a4d014 | ||
![]() |
e4d233afa8 | ||
![]() |
b131b255ec | ||
![]() |
20bdb9ff35 | ||
![]() |
666ef7a978 | ||
![]() |
24a97347df | ||
![]() |
0825d5c64e | ||
![]() |
535e752ec7 | ||
![]() |
b611a58fce | ||
![]() |
24e54554ad | ||
![]() |
d23fca4dd1 | ||
![]() |
729e2f5248 | ||
![]() |
437723c6a6 | ||
![]() |
a30c8205b1 | ||
![]() |
c50cf78bb4 | ||
![]() |
be52ba0ea9 | ||
![]() |
55e9ebc4d2 | ||
![]() |
29c3fb0f92 | ||
![]() |
7c3cd9d88d | ||
![]() |
4881d699e3 | ||
![]() |
414db83359 | ||
![]() |
cd4f6e19f4 | ||
![]() |
da709cbbd1 | ||
![]() |
ee9ca16eb5 | ||
![]() |
399efca411 | ||
![]() |
f8bccf9e79 | ||
![]() |
87aab72b63 | ||
![]() |
24688ba18e | ||
![]() |
e8086b6a6f | ||
![]() |
4358437278 | ||
![]() |
e0a9c57a54 | ||
![]() |
e63953ecbc | ||
![]() |
72af200190 | ||
![]() |
2094ae534b | ||
![]() |
f6d6fd179f | ||
![]() |
153ebb2a20 | ||
![]() |
5d58e52eea | ||
![]() |
5038f9c3c6 | ||
![]() |
b285fda61b | ||
![]() |
6cd38472cd | ||
![]() |
8fd5f53f96 | ||
![]() |
b70eee77ef | ||
![]() |
4148b8c7aa | ||
![]() |
e22dd0c49d | ||
![]() |
30a254f98f | ||
![]() |
8fcb3a017b | ||
![]() |
5e29c7efa9 | ||
![]() |
b6cc3e3ef0 | ||
![]() |
184bdc0c85 | ||
![]() |
f7fb731dc8 | ||
![]() |
77977f64a3 | ||
![]() |
e8da573ba2 | ||
![]() |
07332bf155 | ||
![]() |
f3c7583bf7 | ||
![]() |
1cc02415d3 | ||
![]() |
6ca3f06ea0 | ||
![]() |
198e2b7bdf | ||
![]() |
5a68e2c977 | ||
![]() |
d9d29db560 | ||
![]() |
124c6dc2b8 | ||
![]() |
0f3886e053 | ||
![]() |
68bb3558b4 | ||
![]() |
b8bd15aa33 | ||
![]() |
ed39aa6a7c | ||
![]() |
405cae9b5f | ||
![]() |
3ca2cbb3f9 | ||
![]() |
c295ae56ab | ||
![]() |
830364721b | ||
![]() |
19089213e3 | ||
![]() |
b633067e5c | ||
![]() |
1b8874cbd4 | ||
![]() |
a5f8ce85ba | ||
![]() |
9324061d05 | ||
![]() |
eafcbdc65b | ||
![]() |
0175522c17 | ||
![]() |
cff3f51d34 | ||
![]() |
0f580a91c9 | ||
![]() |
389f50b29a | ||
![]() |
b689bb8fcf | ||
![]() |
36f067ede4 | ||
![]() |
c2178622dd | ||
![]() |
014448e7ea | ||
![]() |
08eff0509a | ||
![]() |
62d0882e82 | ||
![]() |
86a574dbbd | ||
![]() |
71ac4620c5 | ||
![]() |
f611049517 | ||
![]() |
45fa8c272f | ||
![]() |
28a1c97571 | ||
![]() |
d9a5ae0cf1 | ||
![]() |
c03849d30b | ||
![]() |
535fe2686b | ||
![]() |
709bc87a36 | ||
![]() |
2812b467ec | ||
![]() |
7d118a5715 | ||
![]() |
8bd7370a02 | ||
![]() |
9fa8a96d09 | ||
![]() |
508d1fffef | ||
![]() |
3633daa814 | ||
![]() |
05346ae9fc | ||
![]() |
ea667cf0b9 | ||
![]() |
048ac3965e | ||
![]() |
276b6f4d1f | ||
![]() |
e765d7749c | ||
![]() |
9a3b4d6df2 | ||
![]() |
529e27992e | ||
![]() |
6c5cf2a0ec | ||
![]() |
a4cb270f09 | ||
![]() |
5160a1f55c | ||
![]() |
6a3a0db338 | ||
![]() |
765d4eb3b4 | ||
![]() |
cc09e24d66 | ||
![]() |
e7848262ea | ||
![]() |
0926202eca | ||
![]() |
e83af02410 | ||
![]() |
74d6a52fa9 | ||
![]() |
5baa975632 | ||
![]() |
4ad49ef07f | ||
![]() |
bc47ecaa57 | ||
![]() |
2bd617ce6e | ||
![]() |
dbaf955525 | ||
![]() |
578ff5b53f | ||
![]() |
e386942ea7 | ||
![]() |
2fdd50f45f | ||
![]() |
4b36770adf | ||
![]() |
54377225ec | ||
![]() |
f020add6be | ||
![]() |
b1a3996cf1 | ||
![]() |
a47a0ed716 | ||
![]() |
91cd584b4b | ||
![]() |
75562efb79 | ||
![]() |
f464bcfc14 | ||
![]() |
4922e575f8 | ||
![]() |
ac08daa64e |
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
18
.github/workflows/ci.yaml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -62,9 +62,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
6
.github/workflows/demo.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.1
|
||||
- uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
63
.github/workflows/nightly.yaml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Nightly
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.10"
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
actions: none
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
name: Nightly
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Download translations
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Bump version
|
||||
run: script/version_bump.js nightly
|
||||
|
||||
- name: Build nightly Python wheels
|
||||
run: |
|
||||
pip install build
|
||||
yarn install
|
||||
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
42
.github/workflows/release.yaml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: 3.8
|
||||
NODE_VERSION: 14
|
||||
PYTHON_VERSION: "3.10"
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@@ -21,21 +21,21 @@ jobs:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -74,33 +74,11 @@ jobs:
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Upload requirements.txt
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: requirements
|
||||
path: ./requirements.txt
|
||||
|
||||
build-wheels:
|
||||
name: Build wheels for ${{ matrix.arch }}
|
||||
needs: wheels-init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
|
||||
tag:
|
||||
- "3.9-alpine3.14"
|
||||
steps:
|
||||
- name: Download requirements.txt
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: requirements
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@master
|
||||
uses: home-assistant/wheels@2022.06.7
|
||||
with:
|
||||
tag: ${{ matrix.tag }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||
abi: cp310
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
requirements: "requirements.txt"
|
||||
|
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v3.0.13
|
||||
uses: actions/stale@v5.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
4
.github/workflows/translations.yaml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
- src/translations/en.json
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14
|
||||
NODE_VERSION: 16
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@@ -27,7 +27,7 @@ module.exports = {
|
||||
version() {
|
||||
const version = fs
|
||||
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
|
||||
.match(/version\W+=\W"(\d{8}\.\d)"/);
|
||||
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
|
||||
if (!version) {
|
||||
throw Error("Version not found");
|
||||
}
|
||||
|
@@ -156,3 +156,12 @@ gulp.task("gen-icons-json", (done) => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-dummy-icons-json", (done) => {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve(OUTPUT_DIR, "iconList.json"), "[]");
|
||||
done();
|
||||
});
|
||||
|
@@ -9,6 +9,7 @@ require("./compress.js");
|
||||
require("./rollup.js");
|
||||
require("./gather-static.js");
|
||||
require("./translations.js");
|
||||
require("./gen-icons-json.js");
|
||||
|
||||
gulp.task(
|
||||
"develop-hassio",
|
||||
@@ -17,6 +18,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"gen-index-hassio-dev",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
@@ -33,6 +35,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-hassio",
|
||||
"gen-dummy-icons-json",
|
||||
"build-supervisor-translations",
|
||||
"copy-translations-supervisor",
|
||||
"build-locale-data",
|
||||
|
@@ -1 +1,30 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"path": "M20,20H7A2,2 0 0,1 5,18V8.94L2.23,5.64C2.09,5.47 2,5.24 2,5A1,1 0 0,1 3,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20M8.5,7A0.5,0.5 0 0,0 8,7.5V8.5A0.5,0.5 0 0,0 8.5,9H18.5A0.5,0.5 0 0,0 19,8.5V7.5A0.5,0.5 0 0,0 18.5,7H8.5M8.5,11A0.5,0.5 0 0,0 8,11.5V12.5A0.5,0.5 0 0,0 8.5,13H18.5A0.5,0.5 0 0,0 19,12.5V11.5A0.5,0.5 0 0,0 18.5,11H8.5M8.5,15A0.5,0.5 0 0,0 8,15.5V16.5A0.5,0.5 0 0,0 8.5,17H13.5A0.5,0.5 0 0,0 14,16.5V15.5A0.5,0.5 0 0,0 13.5,15H8.5Z",
|
||||
"name": "android-messages"
|
||||
},
|
||||
{
|
||||
"path": "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,12L17.5,10.5L15,12V4H20V12Z",
|
||||
"name": "book-variant-multiple"
|
||||
},
|
||||
{
|
||||
"path": "M21,14H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10L8,21V22H16V21L14,18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z",
|
||||
"name": "desktop-mac"
|
||||
},
|
||||
{
|
||||
"path": "M21,14V4H3V14H21M21,2A2,2 0 0,1 23,4V16A2,2 0 0,1 21,18H14L16,21V22H8V21L10,18H3C1.89,18 1,17.1 1,16V4C1,2.89 1.89,2 3,2H21M4,5H15V10H4V5M16,5H20V7H16V5M20,8V13H16V8H20M4,11H9V13H4V11M10,11H15V13H10V11Z",
|
||||
"name": "desktop-mac-dashboard"
|
||||
},
|
||||
{
|
||||
"path": "M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z",
|
||||
"name": "discord"
|
||||
},
|
||||
{
|
||||
"path": "M8.06,7.78C7.5,7.78 7.17,7.73 7.08,7.64L6.66,13.73C7.19,14.05 7.88,14.3 8.72,14.5C9.56,14.71 10.78,14.77 12.38,14.67C13.97,14.58 15.63,14.23 17.34,13.64L16.55,4.22C15.67,5.09 14.38,5.91 12.66,6.66C11.13,7.31 9.81,7.69 8.72,7.78H8.06M7.97,5.34C7.28,5.94 7,6.34 7.13,6.56C7.22,6.78 7.7,6.84 8.58,6.75C9.67,6.66 10.91,6.31 12.28,5.72C13.22,5.31 14.03,4.88 14.72,4.41C15.41,3.94 15.88,3.55 16.13,3.23C16.38,2.92 16.47,2.7 16.41,2.58C16.34,2.42 16.03,2.34 15.47,2.34C14.34,2.34 12.94,2.7 11.25,3.42C9.81,4.05 8.72,4.69 7.97,5.34M17.34,2.2C17.41,2.33 17.44,2.47 17.44,2.63L18.61,17C18.61,18.73 18,20.09 16.83,21.07C15.64,22.05 14.03,22.55 12,22.55C10,22.55 8.4,22.04 7.2,21C6,20 5.39,18.64 5.39,16.92L6.09,6.47C6.09,6.22 6.2,5.94 6.42,5.63C6.64,5.31 6.84,5.06 7.03,4.88L7.36,4.59C8.33,3.78 9.5,3.08 10.88,2.5C11.81,2.08 12.73,1.77 13.62,1.57C14.5,1.37 15.3,1.3 16,1.38C16.71,1.46 17.16,1.73 17.34,2.2Z",
|
||||
"name": "google-home"
|
||||
},
|
||||
{
|
||||
"path": "M19.25,19H4.75V3H19.25M14,22H10V21H14M18,0H6A3,3 0 0,0 3,3V21A3,3 0 0,0 6,24H18A3,3 0 0,0 21,21V3A3,3 0 0,0 18,0Z",
|
||||
"name": "tablet-android"
|
||||
}
|
||||
]
|
||||
|
@@ -59,7 +59,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
hidden: true,
|
||||
radius: 50,
|
||||
friendly_name: "Skolan",
|
||||
friendly_name: "School",
|
||||
icon: "mdi:school",
|
||||
},
|
||||
},
|
||||
@@ -137,7 +137,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
state: "73",
|
||||
attributes: {
|
||||
unit_of_measurement: "%",
|
||||
friendly_name: "oskar batteri",
|
||||
friendly_name: "Oskar battery",
|
||||
device_class: "battery",
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
state: "88",
|
||||
attributes: {
|
||||
unit_of_measurement: "%",
|
||||
friendly_name: "bella batteri",
|
||||
friendly_name: "Bella battery",
|
||||
device_class: "battery",
|
||||
},
|
||||
},
|
||||
@@ -154,7 +154,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
entity_id: "binary_sensor.unifi_camera",
|
||||
state: "off",
|
||||
attributes: {
|
||||
friendly_name: "R\u00f6relsesensor kamera",
|
||||
friendly_name: "Motion sensor camera",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
},
|
||||
@@ -707,7 +707,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
},
|
||||
],
|
||||
cloudiness: 25,
|
||||
friendly_name: "V\u00e4der",
|
||||
friendly_name: "Weather",
|
||||
},
|
||||
},
|
||||
"binary_sensor.ubiquiti_switch": {
|
||||
@@ -731,7 +731,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
round_trip_time_max: "0.626",
|
||||
round_trip_time_mdev: "",
|
||||
round_trip_time_min: "0.358",
|
||||
friendly_name: "Entr\u00e9 kamera",
|
||||
friendly_name: "Entrance camera",
|
||||
device_class: "connectivity",
|
||||
icon: "mdi:cctv",
|
||||
},
|
||||
@@ -797,7 +797,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 34,
|
||||
on: true,
|
||||
friendly_name: "altan_motion_sensor",
|
||||
friendly_name: "Porch motion sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
@@ -807,7 +807,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 88,
|
||||
on: true,
|
||||
friendly_name: "Altand\u00f6rren sensor",
|
||||
friendly_name: "Back door sensor",
|
||||
device_class: "opening",
|
||||
icon: "mdi:door",
|
||||
},
|
||||
@@ -818,7 +818,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 74,
|
||||
on: true,
|
||||
friendly_name: "badrumssensor",
|
||||
friendly_name: "Bathroom motion sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
@@ -829,7 +829,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 47,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor k\u00e4llaren 1",
|
||||
friendly_name: "Basement motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -841,7 +841,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor tv\u00e4ttstugan",
|
||||
friendly_name: "Laundy room motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -863,7 +863,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
attributes: {
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
friendly_name: "R\u00f6relsesensor skafferiet",
|
||||
friendly_name: "Pantry motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -875,7 +875,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 60,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "R\u00f6relsesensor k\u00e4llaren 2",
|
||||
friendly_name: "Stair motion sensor",
|
||||
device_class: "motion",
|
||||
icon: "mdi:walk",
|
||||
},
|
||||
@@ -887,7 +887,7 @@ export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||
battery_level: 47,
|
||||
on: true,
|
||||
dark: true,
|
||||
friendly_name: "B\u00e4nksensor",
|
||||
friendly_name: "Bench sensor",
|
||||
device_class: "motion",
|
||||
},
|
||||
},
|
||||
|
@@ -277,7 +277,7 @@ export const demoLovelaceKernehed: DemoConfig["lovelace"] = () => ({
|
||||
],
|
||||
show_header_toggle: false,
|
||||
type: "entities",
|
||||
title: "Bandbredd",
|
||||
title: "Bandwidth",
|
||||
},
|
||||
// {
|
||||
// title: "Updater",
|
||||
|
@@ -1,5 +1,4 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
|
||||
import { navigate } from "../../src/common/navigate";
|
||||
import {
|
||||
@@ -7,9 +6,14 @@ import {
|
||||
provideHass,
|
||||
} from "../../src/fake_data/provide_hass";
|
||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import "../../src/resources/compatibility";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
@@ -20,9 +24,6 @@ import { mockShoppingList } from "./stubs/shopping_list";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { mockConfig } from "./stubs/config";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
|
||||
class HaDemo extends HomeAssistantAppEl {
|
||||
protected async _initializeHass() {
|
||||
@@ -51,8 +52,36 @@ class HaDemo extends HomeAssistantAppEl {
|
||||
mockMediaPlayer(hass);
|
||||
mockFrontend(hass);
|
||||
mockEnergy(hass);
|
||||
mockConfig(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.grid_fossil_fuel_percentage",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
has_entity_name: false,
|
||||
},
|
||||
]);
|
||||
|
||||
hass.addEntities(energyEntities());
|
||||
|
||||
|
@@ -1,41 +0,0 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfig = (hass: MockHomeAssistant) => {
|
||||
hass.mockAPI("config/config_entries/entry", () => [
|
||||
{
|
||||
entry_id: "co2signal",
|
||||
domain: "co2signal",
|
||||
title: "CO2 Signal",
|
||||
source: "user",
|
||||
state: "loaded",
|
||||
supports_options: false,
|
||||
supports_unload: true,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
},
|
||||
]);
|
||||
hass.mockWS("config/entity_registry/list", () => [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
device_id: "co2signal",
|
||||
area_id: null,
|
||||
disabled_by: null,
|
||||
entity_id: "sensor.grid_fossil_fuel_percentage",
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "co2signal",
|
||||
},
|
||||
]);
|
||||
};
|
20
demo/src/stubs/config_entries.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("config_entries/get", () => [
|
||||
{
|
||||
entry_id: "co2signal",
|
||||
domain: "co2signal",
|
||||
title: "CO2 Signal",
|
||||
source: "user",
|
||||
state: "loaded",
|
||||
supports_options: false,
|
||||
supports_remove_device: false,
|
||||
supports_unload: true,
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
disabled_by: null,
|
||||
reason: null,
|
||||
},
|
||||
]);
|
||||
};
|
@@ -4,4 +4,6 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
export const mockEntityRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: EntityRegistryEntry[] = []
|
||||
) => hass.mockWS("config/entity_registry/list", () => data);
|
||||
) => {
|
||||
hass.mockWS("config/entity_registry/list", () => data);
|
||||
};
|
||||
|
@@ -466,6 +466,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
return results;
|
||||
}
|
||||
);
|
||||
mockHass.mockWS("recorder/get_statistics_metadata", () => []);
|
||||
mockHass.mockWS("history/list_statistic_ids", () => []);
|
||||
mockHass.mockWS(
|
||||
"history/statistics_during_period",
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 25 KiB |
@@ -8,7 +8,7 @@ module.exports = [
|
||||
{
|
||||
category: "lovelace",
|
||||
// Label for in the sidebar
|
||||
header: "Lovelace",
|
||||
header: "Dashboards",
|
||||
// Specify order of pages. Any pages in the category folder but not listed here will
|
||||
// automatically be added after the pages listed here.
|
||||
pages: ["introduction"],
|
||||
@@ -34,7 +34,7 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
category: "misc",
|
||||
header: "Miscelaneous",
|
||||
header: "Miscellaneous",
|
||||
},
|
||||
{
|
||||
category: "brand",
|
||||
|
@@ -31,7 +31,7 @@ const ENTITIES = [
|
||||
friendly_name: "Office Light",
|
||||
}),
|
||||
getEntity("fan", "kitchen", "on", {
|
||||
friendly_name: "Second Office Fan",
|
||||
friendly_name: "Kitchen Fan",
|
||||
}),
|
||||
getEntity("binary_sensor", "kitchen_door", "on", {
|
||||
friendly_name: "Office Door",
|
||||
@@ -102,7 +102,7 @@ class DemoArea extends LitElement {
|
||||
picture: "/images/office.jpg",
|
||||
},
|
||||
{
|
||||
name: "Second Office",
|
||||
name: "Kitchen",
|
||||
area_id: "kitchen",
|
||||
picture: "/images/kitchen.png",
|
||||
},
|
||||
|
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
Lovelace has many different cards. Each card allows the user to tell
|
||||
Dashboards have many different cards. Each card allows the user to tell
|
||||
a different story about what is going on in their house. These cards
|
||||
are very customizable, as no household is the same.
|
||||
|
||||
This gallery helps our developers and designers to see all the
|
||||
different states that each card can be in.
|
||||
|
||||
Check [the Lovelace documentation](https://www.home-assistant.io/lovelace) for instructions on how to get started with Lovelace.
|
||||
Check [the Dashboards documentation](https://www.home-assistant.io/dashboards/) for instructions on how to get started with Dashboards.
|
||||
|
@@ -194,6 +194,7 @@ const createEntityRegistryEntries = (
|
||||
name: null,
|
||||
icon: null,
|
||||
platform: "updater",
|
||||
has_entity_name: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -69,7 +69,7 @@ const ENTITIES = [
|
||||
effect_list: ["random", "colorloop"],
|
||||
}),
|
||||
getEntity("light", "color_RGB_light", "on", {
|
||||
friendly_name: "Color Effets Light",
|
||||
friendly_name: "Color Effects Light",
|
||||
brightness: 255,
|
||||
rgb_color: [30, 100, 255],
|
||||
supported_features: SUPPORT_EFFECT + SUPPORT_FLASH + SUPPORT_TRANSITION,
|
||||
|
@@ -6,10 +6,8 @@ import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
@@ -23,20 +21,16 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public repo!: HassioAddonRepository;
|
||||
|
||||
@property({ attribute: false }) public addons!: HassioAddonInfo[];
|
||||
@property({ attribute: false }) public addons!: StoreAddon[];
|
||||
|
||||
@property() public filter!: string;
|
||||
|
||||
private _getAddons = memoizeOne(
|
||||
(addons: HassioAddonInfo[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name)
|
||||
);
|
||||
private _getAddons = memoizeOne((addons: StoreAddon[], filter?: string) => {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
);
|
||||
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const repo = this.repo;
|
||||
@@ -87,10 +81,10 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("addon.installed")
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("addon.not_installed")
|
||||
: this.supervisor.localize("addon.not_available")}
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
|
@@ -14,15 +14,15 @@ import memoizeOne from "memoize-one";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import "../../../src/components/search-input";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/search-input";
|
||||
import {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
reloadHassioAddons,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
@@ -66,10 +66,10 @@ class HassioAddonStore extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
let repos: TemplateResult[] = [];
|
||||
|
||||
if (this.supervisor.addon.repositories) {
|
||||
if (this.supervisor.store.repositories) {
|
||||
repos = this.addonRepositories(
|
||||
this.supervisor.addon.repositories,
|
||||
this.supervisor.addon.addons,
|
||||
this.supervisor.store.repositories,
|
||||
this.supervisor.store.addons,
|
||||
this._filter
|
||||
);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class HassioAddonStore extends LitElement {
|
||||
private addonRepositories = memoizeOne(
|
||||
(
|
||||
repositories: HassioAddonRepository[],
|
||||
addons: HassioAddonInfo[],
|
||||
addons: StoreAddon[],
|
||||
filter?: string
|
||||
) =>
|
||||
repositories.sort(sortRepos).map((repo) => {
|
||||
|
@@ -336,7 +336,7 @@ class HassioAddonConfig extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.common.update_available",
|
||||
"addon.failed_to_reset",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
|
@@ -81,7 +81,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_logs",
|
||||
"addon.documentation.get_documentation",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
|
@@ -12,15 +12,17 @@ import { navigate } from "../../../src/common/navigate";
|
||||
import { extractSearchParam } from "../../../src/common/url/search-params";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import {
|
||||
fetchAddonInfo,
|
||||
fetchHassioAddonInfo,
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
setSupervisorOption,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
addStoreRepository,
|
||||
fetchSupervisorStore,
|
||||
StoreAddonDetails,
|
||||
} from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-error-screen";
|
||||
@@ -45,7 +47,9 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public addon?: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon?:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@@ -173,10 +177,10 @@ class HassioAddonDashboard extends LitElement {
|
||||
const requestedAddon = extractSearchParam("addon");
|
||||
const requestedAddonRepository = extractSearchParam("repository_url");
|
||||
if (requestedAddonRepository) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
const storeInfo = await fetchSupervisorStore(this.hass);
|
||||
if (
|
||||
!supervisorInfo.addons_repositories.find(
|
||||
(repo) => repo === requestedAddonRepository
|
||||
!storeInfo.repositories.find(
|
||||
(repo) => repo.source === requestedAddonRepository
|
||||
)
|
||||
) {
|
||||
if (
|
||||
@@ -197,12 +201,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: [
|
||||
...supervisorInfo.addons_repositories,
|
||||
requestedAddonRepository,
|
||||
],
|
||||
});
|
||||
await addStoreRepository(this.hass, requestedAddonRepository);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
}
|
||||
@@ -245,6 +244,8 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
if (path === "uninstall") {
|
||||
window.history.back();
|
||||
} else if (path === "install") {
|
||||
this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug);
|
||||
} else {
|
||||
await this._routeDataChanged();
|
||||
}
|
||||
@@ -262,8 +263,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
|
||||
this.addon = addoninfo;
|
||||
this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon);
|
||||
} catch (err: any) {
|
||||
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
|
||||
this.addon = undefined;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddonDetails } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
HassRouterPage,
|
||||
@@ -20,7 +21,9 @@ class HassioAddonRouter extends HassRouterPage {
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "info",
|
||||
|
@@ -59,7 +59,10 @@ import {
|
||||
fetchHassioStats,
|
||||
HassioStats,
|
||||
} from "../../../../src/data/hassio/common";
|
||||
import { StoreAddon } from "../../../../src/data/supervisor/store";
|
||||
import {
|
||||
StoreAddon,
|
||||
StoreAddonDetails,
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -100,7 +103,9 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public addon!: HassioAddonDetails;
|
||||
@property({ attribute: false }) public addon!:
|
||||
| HassioAddonDetails
|
||||
| StoreAddonDetails;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@@ -143,7 +148,7 @@ class HassioAddonInfo extends LitElement {
|
||||
></update-available-card>
|
||||
`
|
||||
: ""}
|
||||
${!this.addon.protected
|
||||
${"protected" in this.addon && !this.addon.protected
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
@@ -518,7 +523,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div>
|
||||
${this.addon.state === "started"
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("addon.dashboard.hostname")}
|
||||
@@ -669,7 +674,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if (this.addon.state === "started") {
|
||||
if ("state" in this.addon && this.addon.state === "started") {
|
||||
this._metrics = await fetchHassioStats(
|
||||
this.hass,
|
||||
`addons/${this.addon.slug}`
|
||||
@@ -717,18 +722,22 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
|
||||
private get _computeIsRunning(): boolean {
|
||||
return this.addon?.state === "started";
|
||||
return (this.addon as HassioAddonDetails)?.state === "started";
|
||||
}
|
||||
|
||||
private get _pathWebui(): string | null {
|
||||
return (
|
||||
this.addon.webui &&
|
||||
this.addon.webui.replace("[HOST]", document.location.hostname)
|
||||
return (this.addon as HassioAddonDetails).webui!.replace(
|
||||
"[HOST]",
|
||||
document.location.hostname
|
||||
);
|
||||
}
|
||||
|
||||
private get _computeShowWebUI(): boolean | "" | null {
|
||||
return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
|
||||
return (
|
||||
!this.addon.ingress &&
|
||||
(this.addon as HassioAddonDetails).webui &&
|
||||
this._computeIsRunning
|
||||
);
|
||||
}
|
||||
|
||||
private _openIngress(): void {
|
||||
@@ -754,7 +763,8 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _startOnBootToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
boot: this.addon.boot === "auto" ? "manual" : "auto",
|
||||
boot:
|
||||
(this.addon as HassioAddonDetails).boot === "auto" ? "manual" : "auto",
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -776,7 +786,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _watchdogToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
watchdog: !this.addon.watchdog,
|
||||
watchdog: !(this.addon as HassioAddonDetails).watchdog,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -798,7 +808,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _autoUpdateToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
auto_update: !this.addon.auto_update,
|
||||
auto_update: !(this.addon as HassioAddonDetails).auto_update,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -820,7 +830,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _protectionToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetSecurityParams = {
|
||||
protected: !this.addon.protected,
|
||||
protected: !(this.addon as HassioAddonDetails).protected,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonSecurity(this.hass, this.addon.slug, data);
|
||||
@@ -842,7 +852,7 @@ class HassioAddonInfo extends LitElement {
|
||||
private async _panelToggled(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const data: HassioAddonSetOptionParams = {
|
||||
ingress_panel: !this.addon.ingress_panel,
|
||||
ingress_panel: !(this.addon as HassioAddonDetails).ingress_panel,
|
||||
};
|
||||
try {
|
||||
await setHassioAddonOption(this.hass, this.addon.slug, data);
|
||||
@@ -870,7 +880,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
content: extractChangelog(this.addon, content),
|
||||
content: extractChangelog(this.addon as HassioAddonDetails, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -98,9 +98,8 @@ export class HassioBackups extends LitElement {
|
||||
if (backup.content.addons.length !== 0) {
|
||||
for (const addon of backup.content.addons) {
|
||||
content.push(
|
||||
this.supervisor.supervisor.addons.find(
|
||||
(entry) => entry.slug === addon
|
||||
)?.name || addon
|
||||
this.supervisor.addon.addons.find((entry) => entry.slug === addon)
|
||||
?.name || addon
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
|
||||
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
|
||||
const options: Fuse.IFuseOptions<HassioAddonInfo> = {
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: Fuse.IFuseOptions<StoreAddon> = {
|
||||
keys: ["name", "description", "slug"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: 2,
|
||||
|
@@ -96,7 +96,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ["ssl", "share", "media", "addons/local"]
|
||||
);
|
||||
this.addons = _computeAddons(
|
||||
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
|
||||
this.backup ? this.backup.addons : this.supervisor?.addon.addons
|
||||
);
|
||||
this.backupType = this.backup?.type || "full";
|
||||
this.backupName = this.backup?.name || "";
|
||||
@@ -168,23 +168,24 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupType === "partial"
|
||||
? html`<div class="partial-picker">
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@change=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>
|
||||
|
||||
${this.backup?.homeassistant
|
||||
? html`<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
label="Home Assistant"
|
||||
.iconPath=${mdiHomeAssistant}
|
||||
.version=${this.backup
|
||||
? this.backup.homeassistant
|
||||
: this.hass.config.version}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.homeAssistant}
|
||||
@change=${this.toggleHomeAssistant}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${foldersSection?.templates.length
|
||||
? html`
|
||||
<ha-formfield
|
||||
|
@@ -24,7 +24,7 @@ class HassioAddons extends LitElement {
|
||||
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.supervisor.addons?.length
|
||||
${!this.supervisor.addon.addons.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
@@ -34,7 +34,7 @@ class HassioAddons extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: this.supervisor.supervisor.addons
|
||||
: this.supervisor.addon.addons
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
.map(
|
||||
(addon) => html`
|
||||
|
@@ -201,26 +201,24 @@ class HassioBackupDialog
|
||||
}
|
||||
|
||||
if (!this._dialogParams?.onboarding) {
|
||||
this.hass!.callApi(
|
||||
"POST",
|
||||
try {
|
||||
await this.hass!.callApi(
|
||||
"POST",
|
||||
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
).then(
|
||||
() => {
|
||||
this.closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
`hassio/${
|
||||
atLeastVersion(this.hass!.config.version, 2021, 9)
|
||||
? "backups"
|
||||
: "snapshots"
|
||||
}/${this._backup!.slug}/restore/partial`,
|
||||
backupDetails
|
||||
);
|
||||
this.closeDialog();
|
||||
} catch (error: any) {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backupDetails),
|
||||
});
|
||||
|
@@ -15,15 +15,18 @@ import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import {
|
||||
fetchHassioAddonsInfo,
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
} from "../../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import {
|
||||
addStoreRepository,
|
||||
fetchStoreRepositories,
|
||||
removeStoreRepository,
|
||||
} from "../../../../src/data/supervisor/store";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@@ -58,7 +61,13 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private _filteredRepositories = memoizeOne((repos: HassioAddonRepository[]) =>
|
||||
repos
|
||||
.filter((repo) => repo.slug !== "core" && repo.slug !== "local")
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "5c53de3b" // The ESPHome repository
|
||||
)
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
);
|
||||
|
||||
@@ -78,7 +87,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
const repositories = this._filteredRepositories(this._repositories);
|
||||
const usedRepositories = this._filteredUsedRepositories(
|
||||
repositories,
|
||||
this._dialogParams.supervisor.supervisor.addons
|
||||
this._dialogParams.supervisor.addon.addons
|
||||
);
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -215,9 +224,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
try {
|
||||
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
|
||||
|
||||
this._repositories = addonsinfo.repositories;
|
||||
this._repositories = await fetchStoreRepositories(this.hass);
|
||||
|
||||
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
|
||||
} catch (err: any) {
|
||||
@@ -231,14 +238,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._processing = true;
|
||||
const repositories = this._filteredRepositories(this._repositories!);
|
||||
const newRepositories = repositories.map((repo) => repo.source);
|
||||
newRepositories.push(input.value);
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
await addStoreRepository(this.hass, input.value);
|
||||
await this._loadData();
|
||||
|
||||
input.value = "";
|
||||
@@ -250,19 +252,8 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
|
||||
private async _removeRepository(ev: Event) {
|
||||
const slug = (ev.currentTarget as any).slug;
|
||||
const repositories = this._filteredRepositories(this._repositories!);
|
||||
const repository = repositories.find((repo) => repo.slug === slug);
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
const newRepositories = repositories
|
||||
.map((repo) => repo.source)
|
||||
.filter((repo) => repo !== repository.source);
|
||||
|
||||
try {
|
||||
await setSupervisorOption(this.hass, {
|
||||
addons_repositories: newRepositories,
|
||||
});
|
||||
await removeStoreRepository(this.hass, slug);
|
||||
await this._loadData();
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
|
@@ -25,7 +25,7 @@ import {
|
||||
} from "../../src/data/supervisor/supervisor";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { HomeAssistant, Route, TranslationDict } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
|
||||
declare global {
|
||||
@@ -124,9 +124,13 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
|
||||
this.supervisor = {
|
||||
...this.supervisor,
|
||||
localize: await computeLocalize(this.constructor.prototype, language, {
|
||||
[language]: data,
|
||||
}),
|
||||
localize: await computeLocalize<TranslationDict["supervisor"]>(
|
||||
this.constructor.prototype,
|
||||
language,
|
||||
{
|
||||
[language]: data,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
import {
|
||||
UNHEALTHY_REASON_URL,
|
||||
UNSUPPORTED_REASON_URL,
|
||||
} from "../../../src/panels/config/system-health/ha-config-system-health";
|
||||
} from "../../../src/panels/config/repairs/dialog-system-information";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
|
@@ -72,8 +72,8 @@
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "6.7.96",
|
||||
"@mdi/svg": "6.7.96",
|
||||
"@mdi/js": "7.0.96",
|
||||
"@mdi/svg": "7.0.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20220601.0"
|
||||
version = "20220802.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
@@ -23,8 +23,3 @@ include-package-data = true
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["hass_frontend*"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = 3.4
|
||||
show_error_codes = true
|
||||
strict = true
|
||||
|
@@ -24,10 +24,15 @@ function auto(version) {
|
||||
return patch(version);
|
||||
}
|
||||
|
||||
function nightly() {
|
||||
return `${today()}.dev`;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
patch,
|
||||
today,
|
||||
auto,
|
||||
nightly,
|
||||
};
|
||||
|
||||
async function main(args) {
|
||||
@@ -57,7 +62,11 @@ async function main(args) {
|
||||
console.log("Current version:", version);
|
||||
console.log("New version:", newVersion);
|
||||
|
||||
fs.writeFileSync("pyproject.toml", setup.replace(version, newVersion), "utf-8");
|
||||
fs.writeFileSync(
|
||||
"pyproject.toml",
|
||||
setup.replace(version, newVersion),
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
if (!commit) {
|
||||
return;
|
||||
|
2
setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
|
||||
# Keep this file until it does!
|
@@ -47,7 +47,7 @@ import {
|
||||
mdiRobotVacuum,
|
||||
mdiScriptText,
|
||||
mdiSineWave,
|
||||
mdiTextToSpeech,
|
||||
mdiMicrophoneMessage,
|
||||
mdiThermometer,
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
@@ -74,8 +74,9 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
camera: mdiVideo,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
conversation: mdiTextToSpeech,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
counter: mdiCounter,
|
||||
demo: mdiHomeAssistant,
|
||||
fan: mdiFan,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
|
@@ -76,7 +76,11 @@ class Storage {
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
if (value === undefined) {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
} else {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Safari in private mode doesn't allow localstorage
|
||||
}
|
||||
|
@@ -5,8 +5,7 @@ export type LeafletModuleType = typeof import("leaflet");
|
||||
export type LeafletDrawModuleType = typeof import("leaflet-draw");
|
||||
|
||||
export const setupLeafletMap = async (
|
||||
mapElement: HTMLElement,
|
||||
darkMode?: boolean
|
||||
mapElement: HTMLElement
|
||||
): Promise<[Map, LeafletModuleType, TileLayer]> => {
|
||||
if (!mapElement.parentNode) {
|
||||
throw new Error("Cannot setup Leaflet map on disconnected element");
|
||||
@@ -23,7 +22,7 @@ export const setupLeafletMap = async (
|
||||
mapElement.parentNode.appendChild(style);
|
||||
map.setView([52.3731339, 4.8903147], 13);
|
||||
|
||||
const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
|
||||
const tileLayer = createTileLayer(Leaflet).addTo(map);
|
||||
|
||||
return [map, Leaflet, tileLayer];
|
||||
};
|
||||
@@ -31,23 +30,19 @@ export const setupLeafletMap = async (
|
||||
export const replaceTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
map: Map,
|
||||
tileLayer: TileLayer,
|
||||
darkMode: boolean
|
||||
tileLayer: TileLayer
|
||||
): TileLayer => {
|
||||
map.removeLayer(tileLayer);
|
||||
tileLayer = createTileLayer(leaflet, darkMode);
|
||||
tileLayer = createTileLayer(leaflet);
|
||||
tileLayer.addTo(map);
|
||||
return tileLayer;
|
||||
};
|
||||
|
||||
const createTileLayer = (
|
||||
leaflet: LeafletModuleType,
|
||||
darkMode: boolean
|
||||
): TileLayer =>
|
||||
const createTileLayer = (leaflet: LeafletModuleType): TileLayer =>
|
||||
leaflet.tileLayer(
|
||||
`https://{s}.basemaps.cartocdn.com/${
|
||||
darkMode ? "dark_all" : "light_all"
|
||||
}/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`,
|
||||
`https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}${
|
||||
leaflet.Browser.retina ? "@2x.png" : ".png"
|
||||
}`,
|
||||
{
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
|
@@ -8,6 +8,8 @@ import {
|
||||
mdiCalendar,
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiCastOff,
|
||||
mdiChartSankey,
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
mdiCloseCircleOutline,
|
||||
@@ -24,6 +26,15 @@ import {
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSpeaker,
|
||||
mdiSpeakerOff,
|
||||
mdiSpeakerPause,
|
||||
mdiSpeakerPlay,
|
||||
mdiSwapHorizontal,
|
||||
mdiTelevision,
|
||||
mdiTelevisionOff,
|
||||
mdiTelevisionPause,
|
||||
mdiTelevisionPlay,
|
||||
mdiToggleSwitchVariant,
|
||||
mdiToggleSwitchVariantOff,
|
||||
mdiWeatherNight,
|
||||
@@ -125,7 +136,40 @@ export const domainIconWithoutDefault = (
|
||||
}
|
||||
|
||||
case "media_player":
|
||||
return compareState === "playing" ? mdiCastConnected : mdiCast;
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "speaker":
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
return mdiSpeakerPlay;
|
||||
case "paused":
|
||||
return mdiSpeakerPause;
|
||||
case "off":
|
||||
return mdiSpeakerOff;
|
||||
default:
|
||||
return mdiSpeaker;
|
||||
}
|
||||
case "tv":
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
return mdiTelevisionPlay;
|
||||
case "paused":
|
||||
return mdiTelevisionPause;
|
||||
case "off":
|
||||
return mdiTelevisionOff;
|
||||
default:
|
||||
return mdiTelevision;
|
||||
}
|
||||
default:
|
||||
switch (compareState) {
|
||||
case "playing":
|
||||
case "paused":
|
||||
return mdiCastConnected;
|
||||
case "off":
|
||||
return mdiCastOff;
|
||||
default:
|
||||
return mdiCast;
|
||||
}
|
||||
}
|
||||
|
||||
case "switch":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
@@ -153,6 +197,12 @@ export const domainIconWithoutDefault = (
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
: mdiWeatherNight;
|
||||
|
||||
case "switch_as_x":
|
||||
return mdiSwapHorizontal;
|
||||
|
||||
case "threshold":
|
||||
return mdiChartSankey;
|
||||
|
||||
case "update":
|
||||
return compareState === "on"
|
||||
? updateIsInstalling(stateObj as UpdateEntity)
|
||||
|
88
src/common/integrations/protocolIntegrationPicked.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { html } from "lit";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
import { navigate } from "../navigate";
|
||||
|
||||
export const protocolIntegrationPicked = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
slug: string
|
||||
) => {
|
||||
if (slug === "zwave_js") {
|
||||
const entries = await getConfigEntries(hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
|
||||
if (!entries.length) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
showConfirmationDialog(element, {
|
||||
text: hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
||||
{
|
||||
integration: "Z-Wave",
|
||||
supported_hardware_link: html`<a
|
||||
href=${documentationUrl(hass, "/docs/z-wave/controllers")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.supported_hardware"
|
||||
)}</a
|
||||
>`,
|
||||
}
|
||||
),
|
||||
confirmText: hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.proceed"
|
||||
),
|
||||
confirm: () => {
|
||||
fireEvent(element, "handler-picked", {
|
||||
handler: "zwave_js",
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showZWaveJSAddNodeDialog(element, {
|
||||
entry_id: entries[0].entry_id,
|
||||
});
|
||||
} else if (slug === "zha") {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
if (!isComponentLoaded(hass, "zha")) {
|
||||
showConfirmationDialog(element, {
|
||||
text: hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
||||
{
|
||||
integration: "Zigbee",
|
||||
supported_hardware_link: html`<a
|
||||
href=${documentationUrl(
|
||||
hass,
|
||||
"/integrations/zha/#known-working-zigbee-radio-modules"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.supported_hardware"
|
||||
)}</a
|
||||
>`,
|
||||
}
|
||||
),
|
||||
confirmText: hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.proceed"
|
||||
),
|
||||
confirm: () => {
|
||||
fireEvent(element, "handler-picked", {
|
||||
handler: "zha",
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigate("/config/zha/add");
|
||||
}
|
||||
};
|
@@ -5,6 +5,6 @@ export const clamp = (value: number, min: number, max: number) =>
|
||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||
let result: number;
|
||||
result = min ? Math.max(value, min) : value;
|
||||
result = max ? Math.min(value, max) : value;
|
||||
result = max ? Math.min(result, max) : result;
|
||||
return result;
|
||||
};
|
||||
|
@@ -3,10 +3,39 @@ import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-plur
|
||||
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { Resources } from "../../types";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
import { getLocalLanguage } from "../../util/common-translation";
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
// Exclude some patterns from key type checking for now
|
||||
// These are intended to be removed as errors are fixed
|
||||
// Fixing component category will require tighter definition of types from backend and/or web socket
|
||||
type LocalizeKeyExceptions =
|
||||
| `${string}`
|
||||
| `panel.${string}`
|
||||
| `state.${string}`
|
||||
| `state_attributes.${string}`
|
||||
| `state_badge.${string}`
|
||||
| `ui.${string}`
|
||||
| `${keyof TranslationDict["supervisor"]}.${string}`
|
||||
| `component.${string}`;
|
||||
|
||||
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
|
||||
type FlattenObjectKeys<
|
||||
T extends Record<string, any>,
|
||||
Key extends keyof T = keyof T
|
||||
> = Key extends string
|
||||
? T[Key] extends Record<string, unknown>
|
||||
? `${Key}.${FlattenObjectKeys<T[Key]>}`
|
||||
: `${Key}`
|
||||
: never;
|
||||
|
||||
export type LocalizeFunc<
|
||||
Dict extends Record<string, unknown> = TranslationDict
|
||||
> = (
|
||||
key: FlattenObjectKeys<Dict> | LocalizeKeyExceptions,
|
||||
...args: any[]
|
||||
) => string;
|
||||
|
||||
interface FormatType {
|
||||
[format: string]: any;
|
||||
}
|
||||
@@ -65,12 +94,14 @@ export const polyfillsLoaded =
|
||||
* }
|
||||
*/
|
||||
|
||||
export const computeLocalize = async (
|
||||
export const computeLocalize = async <
|
||||
Dict extends Record<string, unknown> = TranslationDict
|
||||
>(
|
||||
cache: any,
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): Promise<LocalizeFunc> => {
|
||||
): Promise<LocalizeFunc<Dict>> => {
|
||||
if (polyfillsLoaded) {
|
||||
await polyfillsLoaded;
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { clamp } from "../../common/number/clamp";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
interface Tooltip extends TooltipModel<any> {
|
||||
top: string;
|
||||
left: string;
|
||||
@@ -186,6 +188,10 @@ export default class HaChartBase extends LitElement {
|
||||
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
);
|
||||
ChartConstructor.defaults.font.family =
|
||||
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
||||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
||||
"Roboto, Noto, sans-serif";
|
||||
|
||||
this.chart = new ChartConstructor(ctx, {
|
||||
type: this.chartType,
|
||||
@@ -324,6 +330,9 @@ export default class HaChartBase extends LitElement {
|
||||
width: 16px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
margin-inline-end: 6px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.chartTooltip .bullet {
|
||||
align-self: baseline;
|
||||
@@ -332,6 +341,9 @@ export default class HaChartBase extends LitElement {
|
||||
:host([rtl]) .chartTooltip .bullet {
|
||||
margin-right: inherit;
|
||||
margin-left: 6px;
|
||||
margin-inline-end: inherit;
|
||||
margin-inline-start: 6px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.chartTooltip {
|
||||
padding: 8px;
|
||||
@@ -368,6 +380,7 @@ export default class HaChartBase extends LitElement {
|
||||
.chartTooltip .title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
direction: ltr;
|
||||
}
|
||||
.chartTooltip .footer {
|
||||
font-weight: 500;
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../common/number/format_number";
|
||||
import { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@@ -34,6 +34,8 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -121,7 +123,13 @@ class StateHistoryChartLine extends LitElement {
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@@ -135,6 +143,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._chartTime = new Date();
|
||||
const endTime = this.endTime;
|
||||
const names = this.names || {};
|
||||
entityStates.forEach((states) => {
|
||||
|
@@ -9,7 +9,7 @@ import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
|
||||
/** Binary sensor device classes for which the static colors for on/off are NOT inverted.
|
||||
@@ -103,6 +103,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions<"timeline">;
|
||||
|
||||
private _chartTime: Date = new Date();
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
@@ -211,7 +213,13 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
};
|
||||
}
|
||||
if (changedProps.has("data")) {
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
this._generateData();
|
||||
}
|
||||
}
|
||||
@@ -224,6 +232,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
stateHistory = [];
|
||||
}
|
||||
|
||||
this._chartTime = new Date();
|
||||
const startTime = this.startTime;
|
||||
const endTime = this.endTime;
|
||||
const labels: string[] = [];
|
||||
|
@@ -186,6 +186,7 @@ class StateHistoryCharts extends LitElement {
|
||||
line-height: 60px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-height: var(--history-max-height);
|
||||
}
|
||||
|
@@ -12,8 +12,10 @@ import { property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeActiveState } from "../../common/entity/compute_active_state";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@@ -93,6 +95,9 @@ export class StateBadge extends LitElement {
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
if (computeDomain(stateObj.entity_id) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
} else if (stateObj.state === "on") {
|
||||
|
@@ -4,8 +4,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { HassioAddonInfo } from "../data/hassio/addon";
|
||||
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
|
||||
import { fetchHassioAddonsInfo, HassioAddonInfo } from "../data/hassio/addon";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
@@ -78,27 +77,27 @@ class HaAddonPicker extends LitElement {
|
||||
private async _getAddons() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
this._addons = supervisorInfo.addons.sort((a, b) =>
|
||||
stringCompare(a.name, b.name)
|
||||
);
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._addons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
.sort((a, b) => stringCompare(a.name, b.name));
|
||||
} else {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.no_supervisor.title"
|
||||
"ui.components.addon-picker.error.no_supervisor.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.no_supervisor.description"
|
||||
"ui.components.addon-picker.error.no_supervisor.description"
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.fetch_addons.title"
|
||||
"ui.components.addon-picker.error.fetch_addons.title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.componencts.addon-picker.error.fetch_addons.description"
|
||||
"ui.components.addon-picker.error.fetch_addons.description"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@@ -76,6 +76,7 @@ class HaAttributes extends LitElement {
|
||||
css`
|
||||
.attribute-container {
|
||||
margin-bottom: 8px;
|
||||
direction: ltr;
|
||||
}
|
||||
.data-entry {
|
||||
display: flex;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "./ha-select";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -8,6 +7,7 @@ import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
class HaBluePrintPicker extends LitElement {
|
||||
@@ -51,7 +51,7 @@ class HaBluePrintPicker extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.label")}
|
||||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.value}
|
||||
@@ -59,11 +59,6 @@ class HaBluePrintPicker extends LitElement {
|
||||
@selected=${this._blueprintChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<mwc-list-item value="">
|
||||
${this.hass.localize(
|
||||
"ui.components.blueprint-picker.select_blueprint"
|
||||
)}
|
||||
</mwc-list-item>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<mwc-list-item .value=${blueprint.path}>
|
||||
|
@@ -67,8 +67,7 @@ export class HaChip extends LitElement {
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip.mdc-chip--selected .mdc-chip__checkmark,
|
||||
.mdc-chip.no-text
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
.mdc-chip .mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
|
||||
margin-right: -4px;
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: 4px;
|
||||
|
@@ -1,17 +1,13 @@
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { css, CSSResult, html } from "lit";
|
||||
import { css, CSSResultGroup, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { HaListItem } from "./ha-list-item";
|
||||
|
||||
@customElement("ha-clickable-list-item")
|
||||
export class HaClickableListItem extends ListItemBase {
|
||||
export class HaClickableListItem extends HaListItem {
|
||||
@property() public href?: string;
|
||||
|
||||
@property({ type: Boolean }) public disableHref = false;
|
||||
|
||||
// property used only in css
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public openNewTab = false;
|
||||
|
||||
@query("a") private _anchor!: HTMLAnchorElement;
|
||||
@@ -39,18 +35,10 @@ export class HaClickableListItem extends ListItemBase {
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -60,19 +48,6 @@ export class HaClickableListItem extends ListItemBase {
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { HassEntities } from "home-assistant-js-websocket";
|
||||
@@ -11,6 +12,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { loadCodeMirror } from "../resources/codemirror.ondemand";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -26,6 +28,12 @@ const saveKeyBinding: KeyBinding = {
|
||||
},
|
||||
};
|
||||
|
||||
const renderIcon = (completion: Completion) => {
|
||||
const icon = document.createElement("ha-icon");
|
||||
icon.icon = completion.label;
|
||||
return icon;
|
||||
};
|
||||
|
||||
@customElement("ha-code-editor")
|
||||
export class HaCodeEditor extends ReactiveElement {
|
||||
public codemirror?: EditorView;
|
||||
@@ -41,12 +49,17 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "autocomplete-entities" })
|
||||
public autocompleteEntities = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autocomplete-icons" })
|
||||
public autocompleteIcons = false;
|
||||
|
||||
@property() public error = false;
|
||||
|
||||
@state() private _value = "";
|
||||
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
|
||||
private _iconList?: Completion[];
|
||||
|
||||
public set value(value: string) {
|
||||
this._value = value;
|
||||
}
|
||||
@@ -151,13 +164,22 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
),
|
||||
];
|
||||
|
||||
if (!this.readOnly && this.autocompleteEntities && this.hass) {
|
||||
extensions.push(
|
||||
this._loadedCodeMirror.autocompletion({
|
||||
override: [this._entityCompletions.bind(this)],
|
||||
maxRenderedOptions: 10,
|
||||
})
|
||||
);
|
||||
if (!this.readOnly) {
|
||||
const completionSources: CompletionSource[] = [];
|
||||
if (this.autocompleteEntities && this.hass) {
|
||||
completionSources.push(this._entityCompletions.bind(this));
|
||||
}
|
||||
if (this.autocompleteIcons) {
|
||||
completionSources.push(this._mdiCompletions.bind(this));
|
||||
}
|
||||
if (completionSources.length > 0) {
|
||||
extensions.push(
|
||||
this._loadedCodeMirror.autocompletion({
|
||||
override: completionSources,
|
||||
maxRenderedOptions: 10,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.codemirror = new this._loadedCodeMirror.EditorView({
|
||||
@@ -187,7 +209,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _entityCompletions(
|
||||
context: CompletionContext
|
||||
): CompletionResult | null | Promise<CompletionResult | null> {
|
||||
const entityWord = context.matchBefore(/[a-z_]{3,}\./);
|
||||
const entityWord = context.matchBefore(/[a-z_]{3,}\.\w*/);
|
||||
|
||||
if (
|
||||
!entityWord ||
|
||||
@@ -205,7 +227,48 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return {
|
||||
from: Number(entityWord.from),
|
||||
options: states,
|
||||
span: /^\w*.\w*$/,
|
||||
span: /^[a-z_]{3,}\.\w*$/,
|
||||
};
|
||||
}
|
||||
|
||||
private _getIconItems = async (): Promise<Completion[]> => {
|
||||
if (!this._iconList) {
|
||||
let iconList: {
|
||||
name: string;
|
||||
keywords: string[];
|
||||
}[];
|
||||
if (__SUPERVISOR__) {
|
||||
iconList = [];
|
||||
} else {
|
||||
iconList = (await import("../../build/mdi/iconList.json")).default;
|
||||
}
|
||||
|
||||
this._iconList = iconList.map((icon) => ({
|
||||
type: "variable",
|
||||
label: `mdi:${icon.name}`,
|
||||
detail: icon.keywords.join(", "),
|
||||
info: renderIcon,
|
||||
}));
|
||||
}
|
||||
|
||||
return this._iconList;
|
||||
};
|
||||
|
||||
private async _mdiCompletions(
|
||||
context: CompletionContext
|
||||
): Promise<CompletionResult | null> {
|
||||
const match = context.matchBefore(/mdi:\S*/);
|
||||
|
||||
if (!match || (match.from === match.to && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconItems = await this._getIconItems();
|
||||
|
||||
return {
|
||||
from: Number(match.from),
|
||||
options: iconItems,
|
||||
span: /^mdi:\S*$/,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -11,7 +11,7 @@ export const createCloseHeading = (
|
||||
hass: HomeAssistant,
|
||||
title: string | TemplateResult
|
||||
) => html`
|
||||
<span class="header_title">${title}</span>
|
||||
<div class="header_title">${title}</div>
|
||||
<ha-icon-button
|
||||
.label=${hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@@ -40,10 +40,13 @@ export class HaDialog extends DialogBase {
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 8px);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 24px);
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
@@ -54,17 +57,23 @@ export class HaDialog extends DialogBase {
|
||||
.mdc-dialog__container {
|
||||
align-items: var(--vertial-align-dialog, center);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
padding: 0 24px 24px 24px;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
display: block;
|
||||
height: 20px;
|
||||
height: 0px;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__content {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: var(--dialog-content-padding, 20px 24px);
|
||||
padding: var(--dialog-content-padding, 24px);
|
||||
}
|
||||
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
|
||||
padding-bottom: max(
|
||||
var(--dialog-content-padding, 20px),
|
||||
var(--dialog-content-padding, 24px),
|
||||
env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
@@ -72,10 +81,7 @@ export class HaDialog extends DialogBase {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-card-border-radius, 4px)
|
||||
);
|
||||
border-radius: var(--ha-dialog-border-radius, 28px);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
@@ -89,8 +95,9 @@ export class HaDialog extends DialogBase {
|
||||
color: inherit;
|
||||
}
|
||||
.header_title {
|
||||
margin-right: 40px;
|
||||
margin-inline-end: 40px;
|
||||
margin-right: 32px;
|
||||
margin-inline-end: 32px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.header_button {
|
||||
|
@@ -19,6 +19,14 @@ export class HaFab extends FabBase {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
? css`
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,10 @@ export const computeInitialHaFormData = (
|
||||
): Record<string, any> => {
|
||||
const data = {};
|
||||
schema.forEach((field) => {
|
||||
if (field.description?.suggested_value) {
|
||||
if (
|
||||
field.description?.suggested_value !== undefined &&
|
||||
field.description?.suggested_value !== null
|
||||
) {
|
||||
data[field.name] = field.description.suggested_value;
|
||||
} else if ("default" in field) {
|
||||
data[field.name] = field.default;
|
||||
|
@@ -3,15 +3,15 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
|
||||
import "../ha-slider";
|
||||
import { HaTextField } from "../ha-textfield";
|
||||
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -105,7 +105,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
return (
|
||||
this.schema.description?.suggested_value ||
|
||||
(this.schema.description?.suggested_value !== undefined &&
|
||||
this.schema.description?.suggested_value !== null) ||
|
||||
this.schema.default ||
|
||||
this.schema.valueMin ||
|
||||
0
|
||||
|
@@ -205,6 +205,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
padding-right: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
@@ -216,6 +219,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
right: 1em;
|
||||
top: 1em;
|
||||
cursor: pointer;
|
||||
inset-inline-end: 1em;
|
||||
inset-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
:host([opened]) ha-svg-icon {
|
||||
color: var(--primary-color);
|
||||
|
@@ -14,6 +14,7 @@ const getAngle = (value: number, min: number, max: number) => {
|
||||
export interface LevelDefinition {
|
||||
level: number;
|
||||
stroke: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-gauge")
|
||||
@@ -38,22 +39,31 @@ export class Gauge extends LitElement {
|
||||
|
||||
@state() private _updated = false;
|
||||
|
||||
@state() private _segment_label? = "";
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Wait for the first render for the initial animation to work
|
||||
afterNextRender(() => {
|
||||
this._updated = true;
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this.getSegmentLabel();
|
||||
this._rescale_svg();
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (!this._updated || !changedProperties.has("value")) {
|
||||
if (
|
||||
!this._updated ||
|
||||
(!changedProperties.has("value") &&
|
||||
!changedProperties.has("label") &&
|
||||
!changedProperties.has("_segment_label"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this.getSegmentLabel();
|
||||
this._rescale_svg();
|
||||
}
|
||||
|
||||
@@ -118,9 +128,11 @@ export class Gauge extends LitElement {
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text class="value-text">
|
||||
${this.valueText || formatNumber(this.value, this.locale)} ${
|
||||
this.label
|
||||
}
|
||||
${
|
||||
this._segment_label
|
||||
? this._segment_label
|
||||
: this.valueText || formatNumber(this.value, this.locale)
|
||||
} ${this._segment_label ? "" : this.label}
|
||||
</text>
|
||||
</svg>`;
|
||||
}
|
||||
@@ -137,6 +149,18 @@ export class Gauge extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private getSegmentLabel() {
|
||||
if (this.levels) {
|
||||
this.levels.sort((a, b) => a.level - b.level);
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
if (this.value >= this.levels[i].level) {
|
||||
return this.levels[i].label;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
|
@@ -29,7 +29,102 @@ interface DeprecatedIcon {
|
||||
};
|
||||
}
|
||||
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {};
|
||||
const mdiDeprecatedIcons: DeprecatedIcon = {
|
||||
"android-messages": {
|
||||
newName: "message-text",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"book-variant-multiple": {
|
||||
newName: "bookmark-box-multiple",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"desktop-mac": {
|
||||
newName: "monitor",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"desktop-mac-dashboard": {
|
||||
newName: "monitor-dashboard",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
discord: {
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"diving-scuba": {
|
||||
newName: "diving-scuba-mask",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"email-send": {
|
||||
newName: "email-arrow-right",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"email-send-outline": {
|
||||
newName: "email-arrow-right-outline",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"email-receive": {
|
||||
newName: "email-arrow-left",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"email-receive-outline": {
|
||||
newName: "email-arrow-left-outline",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"format-textdirection-r-to-l": {
|
||||
newName: "format-pilcrow-arrow-left",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"format-textdirection-l-to-r": {
|
||||
newName: "format-pilcrow-arrow-right",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"google-controller": {
|
||||
newName: "controller",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"google-controller-off": {
|
||||
newName: "controller-off",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"google-home": {
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
lecturn: {
|
||||
newName: "lectern",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
receipt: {
|
||||
newName: "receipt-text",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"receipt-outline": {
|
||||
newName: "receipt-text-outline",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"tablet-android": {
|
||||
newName: "tablet",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"text-to-speech": {
|
||||
newName: "microphone-message",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"text-to-speech-off": {
|
||||
newName: "microphone-message-off",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"timeline-help": {
|
||||
newName: "timeline-question",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"timeline-help-outline": {
|
||||
newName: "timeline-question-outline",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
"vector-point": {
|
||||
newName: "vector-point-select",
|
||||
removeIn: "2022.10",
|
||||
},
|
||||
};
|
||||
|
||||
const chunks: Chunks = {};
|
||||
|
||||
|
42
src/components/ha-list-item.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ListItemBase } from "@material/mwc-list/mwc-list-item-base";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { css, CSSResultGroup } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-list-item")
|
||||
export class HaListItem extends ListItemBase {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
padding-left: var(--mdc-list-side-padding, 20px);
|
||||
padding-right: var(--mdc-list-side-padding, 20px);
|
||||
}
|
||||
:host([graphic="avatar"]:not([twoLine])),
|
||||
:host([graphic="icon"]:not([twoLine])) {
|
||||
height: 48px;
|
||||
}
|
||||
span.material-icons:first-of-type {
|
||||
margin-inline-start: 0px !important;
|
||||
margin-inline-end: var(
|
||||
--mdc-list-item-graphic-margin,
|
||||
16px
|
||||
) !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
span.material-icons:last-of-type {
|
||||
margin-inline-start: auto !important;
|
||||
margin-inline-end: 0px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-list-item": HaListItem;
|
||||
}
|
||||
}
|
@@ -52,6 +52,11 @@ export class HaSelect extends SelectBase {
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
|
||||
inset-inline-start: 48px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select .mdc-select__anchor {
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: 0px;
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
@@ -11,7 +12,11 @@ import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { AreaSelector } from "../../data/selector";
|
||||
import type { AreaSelector } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-area-picker";
|
||||
@@ -29,13 +34,15 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
@@ -45,7 +52,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
(this.selector.area.device?.integration ||
|
||||
@@ -58,7 +65,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.area.device?.integration ||
|
||||
this.selector.area.entity?.integration) &&
|
||||
@@ -77,12 +84,6 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-picker>
|
||||
@@ -98,27 +99,22 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
no-add
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.area.entity?.device_class
|
||||
? [this.selector.area.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-areas-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
const filterIntegration = this.selector.area.entity?.integration;
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.area.entity) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
return filterSelectorEntities(
|
||||
this.selector.area.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
@@ -126,47 +122,17 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.area.device;
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorDevices(
|
||||
this.selector.area.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
};
|
||||
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -2,8 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ConfigEntry } from "../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
import { filterSelectorDevices } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../device/ha-device-picker";
|
||||
@@ -34,12 +35,12 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() public _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
@@ -107,48 +108,17 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
model: filterModel,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.device;
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
if (filterManufacturer && device.manufacturer !== filterManufacturer) {
|
||||
return false;
|
||||
}
|
||||
if (filterModel && device.model !== filterModel) {
|
||||
return false;
|
||||
}
|
||||
if (filterIntegration && this._entitySources && this._entities) {
|
||||
const deviceIntegrations = this._deviceIntegrations(
|
||||
this._entitySources,
|
||||
this._entities
|
||||
);
|
||||
if (!deviceIntegrations?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorDevices(
|
||||
this.selector.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
};
|
||||
|
||||
private _deviceIntegrations = memoizeOne(
|
||||
(entitySources: EntitySources, entities: EntityRegistryEntry[]) => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import { EntitySelector } from "../../data/selector";
|
||||
import type { EntitySelector } from "../../data/selector";
|
||||
import { filterSelectorEntities } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
@@ -73,37 +73,8 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
const {
|
||||
domain: filterDomain,
|
||||
device_class: filterDeviceClass,
|
||||
integration: filterIntegration,
|
||||
} = this.selector.entity;
|
||||
|
||||
if (filterDomain) {
|
||||
const entityDomain = computeStateDomain(entity);
|
||||
if (
|
||||
Array.isArray(filterDomain)
|
||||
? !filterDomain.includes(entityDomain)
|
||||
: entityDomain !== filterDomain
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (
|
||||
filterDeviceClass &&
|
||||
entity.attributes.device_class !== filterDeviceClass
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filterIntegration &&
|
||||
this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
private _filterEntities = (entity: HassEntity): boolean =>
|
||||
filterSelectorEntities(this.selector.entity, entity, this._entitySources);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -4,9 +4,9 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { NumberSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import "../ha-input-helper-text";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@@ -30,21 +30,25 @@ export class HaNumberSelector extends LitElement {
|
||||
const isBox = this.selector.number.mode === "box";
|
||||
|
||||
return html`
|
||||
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
|
||||
<div class="input">
|
||||
${!isBox
|
||||
? html`<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>`
|
||||
? html`
|
||||
${this.label
|
||||
? html`${this.label}${this.required ? " *" : ""}`
|
||||
: ""}
|
||||
<ha-slider
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step ?? 1}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
>
|
||||
</ha-slider>
|
||||
`
|
||||
: ""}
|
||||
<ha-textfield
|
||||
inputMode="numeric"
|
||||
|
@@ -3,17 +3,33 @@ import {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { TargetSelector } from "../../data/selector";
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
} from "../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
TargetSelector,
|
||||
} from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-target-picker";
|
||||
|
||||
@customElement("ha-selector-target")
|
||||
@@ -28,119 +44,82 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _entityPlaformLookup?: Record<string, string>;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
const entityLookup = {};
|
||||
for (const confEnt of entities) {
|
||||
if (!confEnt.platform) {
|
||||
continue;
|
||||
}
|
||||
entityLookup[confEnt.entity_id] = confEnt.platform;
|
||||
}
|
||||
this._entityPlaformLookup = entityLookup;
|
||||
this._entities = entities.filter((entity) => entity.device_id !== null);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (
|
||||
oldSelector !== this.selector &&
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration)
|
||||
) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this.selector.target.device?.integration &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
(this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityRegFilter=${this._filterRegEntities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.includeDeviceClasses=${this.selector.target.entity?.device_class
|
||||
? [this.selector.target.entity.device_class]
|
||||
: undefined}
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (
|
||||
this.selector.target.entity?.integration ||
|
||||
this.selector.target.device?.integration
|
||||
) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
(this.selector.target.entity?.integration ||
|
||||
this.selector.target.device?.integration)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!this.selector.target.entity) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private _filterRegEntities = (entity: EntityRegistryEntry): boolean => {
|
||||
if (this.selector.target.entity?.integration) {
|
||||
if (entity.platform !== this.selector.target.entity.integration) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return filterSelectorEntities(
|
||||
this.selector.target.entity,
|
||||
entity,
|
||||
this._entitySources
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (
|
||||
this.selector.target.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.target.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
if (!this.selector.target.device) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.model &&
|
||||
device.model !== this.selector.target.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.target.device?.integration ||
|
||||
this.selector.target.entity?.integration
|
||||
) {
|
||||
if (
|
||||
!this._configEntries?.some((entry) =>
|
||||
device.config_entries.includes(entry.entry_id)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _loadConfigEntries() {
|
||||
this._configEntries = (await getConfigEntries(this.hass)).filter(
|
||||
(entry) =>
|
||||
entry.domain === this.selector.target.device?.integration ||
|
||||
entry.domain === this.selector.target.entity?.integration
|
||||
const deviceIntegrations =
|
||||
this._entitySources && this._entities
|
||||
? this._deviceIntegrationLookup(this._entitySources, this._entities)
|
||||
: undefined;
|
||||
|
||||
return filterSelectorDevices(
|
||||
this.selector.target.device,
|
||||
device,
|
||||
deviceIntegrations
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
|
@@ -31,6 +31,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
.readOnly=${this.disabled}
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
@value-changed=${this._handleChange}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
|
@@ -21,6 +21,7 @@ import "@polymer/paper-item/paper-icon-item";
|
||||
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -44,7 +45,9 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { updateCanInstall, UpdateEntity } from "../data/update";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -177,7 +180,7 @@ const computePanels = memoizeOne(
|
||||
let Sortable;
|
||||
|
||||
@customElement("ha-sidebar")
|
||||
class HaSidebar extends LitElement {
|
||||
class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||
@@ -192,6 +195,8 @@ class HaSidebar extends LitElement {
|
||||
|
||||
@state() private _updatesCount = 0;
|
||||
|
||||
@state() private _issuesCount = 0;
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
@@ -214,6 +219,16 @@ class HaSidebar extends LitElement {
|
||||
|
||||
private _sortable?;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeRepairsIssueRegistry(this.hass.connection!, (repairs) => {
|
||||
this._issuesCount = repairs.issues.filter(
|
||||
(issue) => !issue.ignored
|
||||
).length;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
@@ -238,6 +253,7 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_issuesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -500,7 +516,7 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null) {
|
||||
return html` <a
|
||||
return html`<a
|
||||
class="configuration-container"
|
||||
role="option"
|
||||
href="/config"
|
||||
@@ -511,17 +527,20 @@ class HaSidebar extends LitElement {
|
||||
>
|
||||
<paper-icon-item class="configuration" role="option">
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand && this._updatesCount > 0
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="configuration-badge" slot="item-icon">
|
||||
${this._updatesCount}
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">${title}</span>
|
||||
${this.alwaysExpand && this._updatesCount > 0
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="configuration-badge">${this._updatesCount}</span>
|
||||
<span class="configuration-badge"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
|
@@ -42,8 +42,8 @@ import "./entity/ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@@ -79,6 +79,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public horizontal = false;
|
||||
|
||||
@state() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@state() private _devices?: {
|
||||
@@ -117,7 +119,26 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (!this._areas || !this._devices || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
return html`<div class="mdc-chip-set items">
|
||||
return html`
|
||||
${this.horizontal
|
||||
? html`
|
||||
<div class="horizontal-container">
|
||||
${this._renderChips()} ${this._renderPicker()}
|
||||
</div>
|
||||
${this._renderItems()}
|
||||
`
|
||||
: html`
|
||||
<div>
|
||||
${this._renderItems()} ${this._renderPicker()}
|
||||
${this._renderChips()}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItems() {
|
||||
return html`
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map((area_id) => {
|
||||
const area = this._areas![area_id];
|
||||
@@ -154,7 +175,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderChips() {
|
||||
return html`
|
||||
<div class="mdc-chip-set">
|
||||
<div
|
||||
class="mdc-chip area_id add"
|
||||
@@ -217,10 +242,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""} `;
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _showPicker(ev) {
|
||||
@@ -289,7 +314,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
class="mdc-chip__icon mdc-chip__icon--trailing"
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
.label=${this.hass.localize("ui.components.target-picker.expand")}
|
||||
.label=${this.hass.localize("ui.components.target-picker.remove")}
|
||||
.path=${mdiClose}
|
||||
hideTooltip
|
||||
.id=${id}
|
||||
@@ -309,48 +334,54 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
private _renderPicker() {
|
||||
switch (this._addMode) {
|
||||
case "area_id":
|
||||
return html`<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>`;
|
||||
return html`
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_area_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-area-picker>
|
||||
`;
|
||||
case "device_id":
|
||||
return html`<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>`;
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"device_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_device_id"
|
||||
)}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityRegFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
></ha-device-picker>
|
||||
`;
|
||||
case "entity_id":
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
return html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
@@ -539,6 +570,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(chipStyles)}
|
||||
.horizontal-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-height: 56px;
|
||||
align-items: center;
|
||||
}
|
||||
.mdc-chip {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
@@ -61,6 +61,11 @@ export class HaTextField extends TextFieldBase {
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-text-field--with-leading-icon {
|
||||
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
|
||||
padding-inline-end: var(--text-field-suffix-padding-right, 16px);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field:not(.mdc-text-field--disabled)
|
||||
.mdc-text-field__affix--suffix {
|
||||
@@ -71,8 +76,14 @@ export class HaTextField extends TextFieldBase {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.mdc-text-field__icon--leading {
|
||||
margin-inline-start: 16px;
|
||||
margin-inline-end: 8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: var(--text-field-text-align);
|
||||
text-align: var(--text-field-text-align, start);
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
@@ -110,7 +121,25 @@ export class HaTextField extends TextFieldBase {
|
||||
inset-inline-end: initial !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
document.dir === "rtl"
|
||||
? css`
|
||||
.mdc-text-field__affix--suffix,
|
||||
.mdc-text-field--with-leading-icon,
|
||||
.mdc-text-field__icon--leading,
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field--with-leading-icon.mdc-text-field--filled
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field__input[type="number"] {
|
||||
direction: rtl;
|
||||
}
|
||||
`
|
||||
: css``,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ export class HaThemePicker extends LitElement {
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize("ui.components.theme_picker.theme")}
|
||||
this.hass!.localize("ui.components.theme-picker.theme")}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@@ -34,7 +34,7 @@ export class HaThemePicker extends LitElement {
|
||||
>
|
||||
<mwc-list-item value="remove"
|
||||
>${this.hass!.localize(
|
||||
"ui.components.theme_picker.no_theme"
|
||||
"ui.components.theme-picker.no_theme"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
|
@@ -41,7 +41,7 @@ export class HaYamlEditor extends LitElement {
|
||||
try {
|
||||
this._yaml =
|
||||
value && !isEmpty(value)
|
||||
? dump(value, { schema: this.yamlSchema })
|
||||
? dump(value, { schema: this.yamlSchema, quotingType: '"' })
|
||||
: "";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -70,6 +70,7 @@ export class HaYamlEditor extends LitElement {
|
||||
.readOnly=${this.readOnly}
|
||||
mode="yaml"
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.error=${this.isValid === false}
|
||||
@value-changed=${this._onChange}
|
||||
dir="ltr"
|
||||
|
@@ -6,21 +6,19 @@ import {
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
TileLayer,
|
||||
} from "leaflet";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
replaceTileLayer,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import "./ha-entity-marker";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
|
||||
import "./ha-entity-marker";
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
@@ -60,8 +58,6 @@ export class HaMap extends ReactiveElement {
|
||||
|
||||
private Leaflet?: LeafletModuleType;
|
||||
|
||||
private _tileLayer?: TileLayer;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _mapItems: Array<Marker | Circle> = [];
|
||||
@@ -142,12 +138,6 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||
this._tileLayer = replaceTileLayer(
|
||||
this.Leaflet!,
|
||||
this.leafletMap!,
|
||||
this._tileLayer!,
|
||||
darkMode
|
||||
);
|
||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||
}
|
||||
|
||||
@@ -159,10 +149,7 @@ export class HaMap extends ReactiveElement {
|
||||
this.shadowRoot!.append(map);
|
||||
}
|
||||
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
|
||||
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
|
||||
map,
|
||||
darkMode
|
||||
);
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map);
|
||||
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
|
||||
this._loaded = true;
|
||||
}
|
||||
@@ -466,6 +453,7 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
#map.dark {
|
||||
background: #090909;
|
||||
--map-filter: invert(0.9) hue-rotate(170deg) grayscale(0.7);
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
@@ -473,6 +461,13 @@ export class HaMap extends ReactiveElement {
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-tile-pane {
|
||||
filter: var(--map-filter);
|
||||
}
|
||||
.dark .leaflet-bar a {
|
||||
background-color: var(--card-background-color, #1c1c1c);
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ declare global {
|
||||
class BrowseMediaTTS extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property() public item;
|
||||
@property() public item!: MediaPlayerItem;
|
||||
|
||||
@property() public action!: MediaPlayerBrowseAction;
|
||||
|
||||
|
@@ -116,9 +116,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
// @ts-ignore
|
||||
private _intersectionObserver?: IntersectionObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._attachResizeObserver());
|
||||
@@ -128,9 +125,6 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
if (this._intersectionObserver) {
|
||||
this._intersectionObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
@@ -485,7 +479,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.layout=${grid({
|
||||
itemSize: {
|
||||
width: "175px",
|
||||
height: "225px",
|
||||
height:
|
||||
childrenMediaClass.thumbnail_ratio === "portrait"
|
||||
? "312px"
|
||||
: "225px",
|
||||
},
|
||||
gap: "16px",
|
||||
flex: { preserve: "aspect-ratio" },
|
||||
|
@@ -8,6 +8,7 @@ import { fetchUsers, User } from "../../data/user";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-select";
|
||||
import "./ha-user-badge";
|
||||
import "../ha-list-item";
|
||||
|
||||
class HaUserPicker extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
@@ -48,14 +49,14 @@ class HaUserPicker extends LitElement {
|
||||
: ""}
|
||||
${this._sortedUsers(this.users).map(
|
||||
(user) => html`
|
||||
<mwc-list-item graphic="avatar" .value=${user.id}>
|
||||
<ha-list-item graphic="avatar" .value=${user.id}>
|
||||
<ha-user-badge
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
slot="graphic"
|
||||
></ha-user-badge>
|
||||
${user.name}
|
||||
</mwc-list-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ApplicationCredentialsDomainConfig {
|
||||
description_placeholders: string;
|
||||
}
|
||||
|
||||
export interface ApplicationCredentialsConfig {
|
||||
domains: string[];
|
||||
integrations: Record<string, ApplicationCredentialsDomainConfig>;
|
||||
}
|
||||
|
||||
export interface ApplicationCredential {
|
||||
|
@@ -3,6 +3,8 @@ import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
|
||||
export interface AreaRegistryEntry {
|
||||
area_id: string;
|
||||
@@ -10,6 +12,14 @@ export interface AreaRegistryEntry {
|
||||
picture: string | null;
|
||||
}
|
||||
|
||||
export interface AreaEntityLookup {
|
||||
[areaId: string]: EntityRegistryEntry[];
|
||||
}
|
||||
|
||||
export interface AreaDeviceLookup {
|
||||
[areaId: string]: DeviceRegistryEntry[];
|
||||
}
|
||||
|
||||
export interface AreaRegistryEntryMutableParams {
|
||||
name: string;
|
||||
picture?: string | null;
|
||||
@@ -79,3 +89,35 @@ export const subscribeAreaRegistry = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): AreaEntityLookup => {
|
||||
const areaEntityLookup: AreaEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.area_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.area_id in areaEntityLookup)) {
|
||||
areaEntityLookup[entity.area_id] = [];
|
||||
}
|
||||
areaEntityLookup[entity.area_id].push(entity);
|
||||
}
|
||||
return areaEntityLookup;
|
||||
};
|
||||
|
||||
export const getAreaDeviceLookup = (
|
||||
devices: DeviceRegistryEntry[]
|
||||
): AreaDeviceLookup => {
|
||||
const areaDeviceLookup: AreaDeviceLookup = {};
|
||||
for (const device of devices) {
|
||||
if (!device.area_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(device.area_id in areaDeviceLookup)) {
|
||||
areaDeviceLookup[device.area_id] = [];
|
||||
}
|
||||
areaDeviceLookup[device.area_id].push(device);
|
||||
}
|
||||
return areaDeviceLookup;
|
||||
};
|
||||
|
@@ -158,8 +158,14 @@ export const getRecentWithCache = (
|
||||
}
|
||||
const stateHistory = computeHistory(hass, fetchedHistory, localize);
|
||||
if (appendingToCache) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
if (stateHistory.line.length) {
|
||||
mergeLine(stateHistory.line, cache.data.line);
|
||||
}
|
||||
if (stateHistory.timeline.length) {
|
||||
mergeTimeline(stateHistory.timeline, cache.data.timeline);
|
||||
// Replace the timeline array to force an update
|
||||
cache.data.timeline = [...cache.data.timeline];
|
||||
}
|
||||
pruneStartTime(startTime, cache.data);
|
||||
} else {
|
||||
cache.data = stateHistory;
|
||||
@@ -191,6 +197,8 @@ const mergeLine = (
|
||||
oldLine.data.push(entity);
|
||||
}
|
||||
});
|
||||
// Replace the cached line data to force an update
|
||||
oldLine.data = [...oldLine.data];
|
||||
} else {
|
||||
cacheLines.push(line);
|
||||
}
|
||||
|
@@ -41,6 +41,12 @@ export interface WebRtcAnswer {
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export const cameraUrlWithWidthHeight = (
|
||||
base_url: string,
|
||||
width: number,
|
||||
height: number
|
||||
) => `${base_url}&width=${width}&height=${height}`;
|
||||
|
||||
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||
|
||||
@@ -57,7 +63,7 @@ export const fetchThumbnailUrlWithCache = async (
|
||||
hass,
|
||||
entityId
|
||||
);
|
||||
return `${base_url}&width=${width}&height=${height}`;
|
||||
return cameraUrlWithWidthHeight(base_url, width, height);
|
||||
};
|
||||
|
||||
export const fetchThumbnailUrl = async (
|
||||
|
@@ -11,7 +11,8 @@ export interface ConfigEntry {
|
||||
| "migration_error"
|
||||
| "setup_retry"
|
||||
| "not_loaded"
|
||||
| "failed_unload";
|
||||
| "failed_unload"
|
||||
| "setup_in_progress";
|
||||
supports_options: boolean;
|
||||
supports_remove_device: boolean;
|
||||
supports_unload: boolean;
|
||||
@@ -28,29 +29,38 @@ export type ConfigEntryMutableParams = Partial<
|
||||
>
|
||||
>;
|
||||
|
||||
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
|
||||
export const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"migration_error",
|
||||
"setup_error",
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
|
||||
export const RECOVERABLE_STATES: ConfigEntry["state"][] = [
|
||||
"not_loaded",
|
||||
"loaded",
|
||||
"setup_error",
|
||||
"setup_retry",
|
||||
];
|
||||
|
||||
export const getConfigEntries = (
|
||||
hass: HomeAssistant,
|
||||
filters?: { type?: "helper" | "integration"; domain?: string }
|
||||
): Promise<ConfigEntry[]> => {
|
||||
const params = new URLSearchParams();
|
||||
const params: any = {};
|
||||
if (filters) {
|
||||
if (filters.type) {
|
||||
params.append("type", filters.type);
|
||||
params.type_filter = filters.type;
|
||||
}
|
||||
if (filters.domain) {
|
||||
params.append("domain", filters.domain);
|
||||
params.domain = filters.domain;
|
||||
}
|
||||
}
|
||||
return hass.callApi<ConfigEntry[]>(
|
||||
"GET",
|
||||
`config/config_entries/entry?${params.toString()}`
|
||||
);
|
||||
return hass.callWS<ConfigEntry[]>({
|
||||
type: "config_entries/get",
|
||||
...params,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateConfigEntry = (
|
||||
|
@@ -6,16 +6,17 @@ import { DataEntryFlowProgress, DataEntryFlowStep } from "./data_entry_flow";
|
||||
import { domainToName } from "./integration";
|
||||
|
||||
export const DISCOVERY_SOURCES = [
|
||||
"usb",
|
||||
"unignore",
|
||||
"bluetooth",
|
||||
"dhcp",
|
||||
"homekit",
|
||||
"ssdp",
|
||||
"zeroconf",
|
||||
"discovery",
|
||||
"hassio",
|
||||
"homekit",
|
||||
"integration_discovery",
|
||||
"mqtt",
|
||||
"hassio",
|
||||
"ssdp",
|
||||
"unignore",
|
||||
"usb",
|
||||
"zeroconf",
|
||||
];
|
||||
|
||||
export const ATTENTION_SOURCES = ["reauth"];
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
id: string;
|
||||
@@ -20,7 +21,7 @@ export interface DeviceRegistryEntry {
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
entry_type: "service" | null;
|
||||
disabled_by: string | null;
|
||||
disabled_by: "user" | "integration" | "config_entry" | null;
|
||||
configuration_url: string | null;
|
||||
}
|
||||
|
||||
@@ -126,3 +127,39 @@ export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
|
||||
entries.sort((entry1, entry2) =>
|
||||
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||
);
|
||||
|
||||
export const getDeviceEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): DeviceEntityLookup => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
return deviceEntityLookup;
|
||||
};
|
||||
|
||||
export const getDeviceIntegrationLookup = (
|
||||
entitySources: EntitySources,
|
||||
entities: EntityRegistryEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
if (!source?.domain || entity.device_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
};
|
||||
|