mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-03 18:34:52 +00:00
Compare commits
250 Commits
20240306.0
...
fix-backgr
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cc1a0b24f0 | ||
![]() |
3a4e9b6856 | ||
![]() |
5f5ac5419b | ||
![]() |
92b7a3b477 | ||
![]() |
00837acdfc | ||
![]() |
7704be12b1 | ||
![]() |
712ddb531b | ||
![]() |
d52afc3f71 | ||
![]() |
92f6083e0b | ||
![]() |
5751fdbe56 | ||
![]() |
3b5b3f3bb6 | ||
![]() |
1a6d96cf3a | ||
![]() |
034fd9b4df | ||
![]() |
eb79a1e7d7 | ||
![]() |
e25d4f17aa | ||
![]() |
ccde9cceee | ||
![]() |
578d3c4260 | ||
![]() |
bfdc9a3d86 | ||
![]() |
5315545a4d | ||
![]() |
82a3b9d80f | ||
![]() |
3de985a3b8 | ||
![]() |
567ee8000d | ||
![]() |
03939001b2 | ||
![]() |
30d18050d1 | ||
![]() |
95caf8c7df | ||
![]() |
6c1f328d71 | ||
![]() |
bb20ab8c2c | ||
![]() |
17ad3a87f3 | ||
![]() |
ed7c9c33b9 | ||
![]() |
59b66219cb | ||
![]() |
1e2c1d1464 | ||
![]() |
5b86b1277f | ||
![]() |
41fdf31e34 | ||
![]() |
9bef5c2af9 | ||
![]() |
ed1a69071b | ||
![]() |
56d328b4db | ||
![]() |
33c7e0fa2d | ||
![]() |
a434bfd944 | ||
![]() |
21ed8e4206 | ||
![]() |
169d782580 | ||
![]() |
8a015f4e38 | ||
![]() |
cbb08c6202 | ||
![]() |
6301bc713c | ||
![]() |
a5d7043ce4 | ||
![]() |
912d2cbd79 | ||
![]() |
48ee3a34eb | ||
![]() |
21263a1ffb | ||
![]() |
db59e138e9 | ||
![]() |
bc8012dcc9 | ||
![]() |
d8b43597a0 | ||
![]() |
871949e760 | ||
![]() |
4fb42d3545 | ||
![]() |
2e58d6656c | ||
![]() |
a3024b38e9 | ||
![]() |
85f2016371 | ||
![]() |
1ce3347c2e | ||
![]() |
4f8415e8a7 | ||
![]() |
b202a36feb | ||
![]() |
7e3e224746 | ||
![]() |
503a7979d0 | ||
![]() |
f3ba6e7996 | ||
![]() |
f13dcb4139 | ||
![]() |
e8dc61ec36 | ||
![]() |
88c59c5c13 | ||
![]() |
85f80ff863 | ||
![]() |
d56abe6b72 | ||
![]() |
bc14b8468d | ||
![]() |
f924f81ec1 | ||
![]() |
3a6382df55 | ||
![]() |
1dba049038 | ||
![]() |
f539516252 | ||
![]() |
abd02eda0f | ||
![]() |
99695d6cb3 | ||
![]() |
cb1c2b59df | ||
![]() |
8368f977b9 | ||
![]() |
e05595f318 | ||
![]() |
11cf2ec39d | ||
![]() |
e5c43fcfcd | ||
![]() |
520581c165 | ||
![]() |
d1119a3b61 | ||
![]() |
5dd029cc05 | ||
![]() |
510e010f97 | ||
![]() |
1300cffa3b | ||
![]() |
8fbcbb0b68 | ||
![]() |
7b26c1ffcb | ||
![]() |
d3e62454a5 | ||
![]() |
6b8f4e92a7 | ||
![]() |
b590b21183 | ||
![]() |
a08484f450 | ||
![]() |
2978ca13c5 | ||
![]() |
31c0850b14 | ||
![]() |
1d85f0717a | ||
![]() |
55c8589841 | ||
![]() |
4687add37a | ||
![]() |
c25e23ccd6 | ||
![]() |
e42ddb8f0f | ||
![]() |
705c0e58fc | ||
![]() |
7427e17926 | ||
![]() |
2c4b31dcaa | ||
![]() |
ae8671af96 | ||
![]() |
f5ff55abc5 | ||
![]() |
b662512995 | ||
![]() |
64c3fb1723 | ||
![]() |
fb99dc4cd0 | ||
![]() |
e08a0c44ba | ||
![]() |
68935d46ce | ||
![]() |
141c8c5192 | ||
![]() |
7ca5467f4c | ||
![]() |
5de53964d9 | ||
![]() |
8d8807e659 | ||
![]() |
9347944cbd | ||
![]() |
480448acbb | ||
![]() |
202fa82646 | ||
![]() |
feecc9f838 | ||
![]() |
2f9e667517 | ||
![]() |
5547bc7356 | ||
![]() |
eb4ae926b7 | ||
![]() |
b239ec2b71 | ||
![]() |
e9cac94aee | ||
![]() |
5289cd3af1 | ||
![]() |
45a5c1c235 | ||
![]() |
db3709952c | ||
![]() |
447932eedb | ||
![]() |
10cc3bdd3f | ||
![]() |
6ee2bfed36 | ||
![]() |
01efb831b7 | ||
![]() |
9e1e20bd94 | ||
![]() |
869ace74ad | ||
![]() |
94d56367fc | ||
![]() |
68a5ba668e | ||
![]() |
b2b590cf67 | ||
![]() |
6f7c071769 | ||
![]() |
c1a7164ce7 | ||
![]() |
b77839c139 | ||
![]() |
e2f2a9322c | ||
![]() |
e4bd6c885d | ||
![]() |
8201701d17 | ||
![]() |
a5e6b78e1d | ||
![]() |
027eccba06 | ||
![]() |
12f10513f0 | ||
![]() |
9907ed51f0 | ||
![]() |
90e9f79841 | ||
![]() |
c30b9cdfcf | ||
![]() |
7e1fa0cf38 | ||
![]() |
b6587488d4 | ||
![]() |
552eeeddf6 | ||
![]() |
cbc150bad2 | ||
![]() |
8a4ed121b5 | ||
![]() |
a9793dc0a5 | ||
![]() |
c9deef84ca | ||
![]() |
1582aaeb4c | ||
![]() |
707520c15c | ||
![]() |
d5de435f06 | ||
![]() |
9e3dfaa400 | ||
![]() |
7aa92ec249 | ||
![]() |
2fdcd40f00 | ||
![]() |
3b15b786ff | ||
![]() |
b212b30e58 | ||
![]() |
6fd89f8585 | ||
![]() |
0406d21703 | ||
![]() |
293f89a07b | ||
![]() |
520a0b4075 | ||
![]() |
488602e232 | ||
![]() |
1e8d353162 | ||
![]() |
b3718b8b4a | ||
![]() |
097cba5c60 | ||
![]() |
fa6d8d0891 | ||
![]() |
31797c55df | ||
![]() |
56a23c5c3d | ||
![]() |
adc89f1487 | ||
![]() |
7facc375bc | ||
![]() |
91d3fb0ea8 | ||
![]() |
4ab0047dc1 | ||
![]() |
279eeaa442 | ||
![]() |
d4d0fb2a03 | ||
![]() |
d56fe8a542 | ||
![]() |
292701925d | ||
![]() |
52fc854cc3 | ||
![]() |
90ca039768 | ||
![]() |
9e81055070 | ||
![]() |
cea402ebf8 | ||
![]() |
6b939b95c0 | ||
![]() |
b24621d1ea | ||
![]() |
cc0fde2c08 | ||
![]() |
3732998fb7 | ||
![]() |
c699e265ef | ||
![]() |
98bb726f1a | ||
![]() |
c132e7ed85 | ||
![]() |
298cebe17f | ||
![]() |
d03825d200 | ||
![]() |
d9ab9db211 | ||
![]() |
58a607561a | ||
![]() |
0ae1f11ffc | ||
![]() |
db48c5a6a3 | ||
![]() |
effefdbff1 | ||
![]() |
233c969402 | ||
![]() |
3b885dd01f | ||
![]() |
52c8554d89 | ||
![]() |
b55baef985 | ||
![]() |
b593b15f27 | ||
![]() |
00669ac0c3 | ||
![]() |
a5bcf87c08 | ||
![]() |
8ca5b7528b | ||
![]() |
d951e68c10 | ||
![]() |
32e8d2043c | ||
![]() |
bf028915ec | ||
![]() |
b03f483e4f | ||
![]() |
6f6202eb69 | ||
![]() |
7ab2d1496e | ||
![]() |
acc229a7e1 | ||
![]() |
64ffa86fe3 | ||
![]() |
8b77024fb9 | ||
![]() |
42aa18ac16 | ||
![]() |
1b7742ef7f | ||
![]() |
0c6bf701c7 | ||
![]() |
05e2e305e4 | ||
![]() |
5523cd6203 | ||
![]() |
50da4bcd37 | ||
![]() |
b99072d986 | ||
![]() |
b9a7a7c422 | ||
![]() |
88ccbcd883 | ||
![]() |
b5bb6c6fe5 | ||
![]() |
19a3810168 | ||
![]() |
8ccc38eb00 | ||
![]() |
70146a08c1 | ||
![]() |
19d50b9c92 | ||
![]() |
05c1328ca7 | ||
![]() |
99c2dd9765 | ||
![]() |
edbe6851f7 | ||
![]() |
a7867a9253 | ||
![]() |
94e70f81ed | ||
![]() |
3d8654253a | ||
![]() |
39bd07de73 | ||
![]() |
3202ea55d2 | ||
![]() |
329a8c0c90 | ||
![]() |
c05824c641 | ||
![]() |
3abdffda9c | ||
![]() |
67da851efc | ||
![]() |
5463a27255 | ||
![]() |
ec0434c9b0 | ||
![]() |
7d8cb5c863 | ||
![]() |
4f01348ffb | ||
![]() |
2af3400464 | ||
![]() |
b6e220a4c5 | ||
![]() |
d5d45f100e | ||
![]() |
6b9ca60c47 | ||
![]() |
bc445a1e27 | ||
![]() |
a087b4c43e | ||
![]() |
8f67ddf968 | ||
![]() |
9ef07484dd |
@@ -1,5 +1,5 @@
|
|||||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
|
FROM mcr.microsoft.com/devcontainers/python:3.12
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@v4.0.0
|
uses: actions/cache@v4.0.2
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
- cron: "0 1 * * *"
|
- cron: "0 1 * * *"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.12"
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHON_VERSION: "3.11"
|
PYTHON_VERSION: "3.12"
|
||||||
NODE_OPTIONS: --max_old_space_size=6144
|
NODE_OPTIONS: --max_old_space_size=6144
|
||||||
|
|
||||||
# Set default workflow permissions
|
# Set default workflow permissions
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@v0.1.15
|
uses: softprops/action-gh-release@v2.0.4
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.1
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/simple-tooltip.js b/simple-tooltip.js
|
|
||||||
index 78a87f6a223925f0e29fbedb268c85a142ec6985..3d686dd6a3d5a93342b4b01408089fc316b408ca 100644
|
|
||||||
--- a/simple-tooltip.js
|
|
||||||
+++ b/simple-tooltip.js
|
|
||||||
@@ -195,6 +195,8 @@ class SimpleTooltip extends LitElement {
|
|
||||||
.hidden {
|
|
||||||
position: absolute;
|
|
||||||
left: -10000px;
|
|
||||||
+ inset-inline-start: -10000px;
|
|
||||||
+ inset-inline-end: initial;
|
|
||||||
top: auto;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.1.0.cjs
|
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||||
|
@@ -72,6 +72,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
id: "sensor.co2_intensity",
|
id: "sensor.co2_intensity",
|
||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
labels: [],
|
||||||
|
categories: {},
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
@@ -88,6 +90,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
id: "sensor.co2_intensity",
|
id: "sensor.co2_intensity",
|
||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
labels: [],
|
||||||
|
categories: {},
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
|
@@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|||||||
export const mockAreaRegistry = (
|
export const mockAreaRegistry = (
|
||||||
hass: MockHomeAssistant,
|
hass: MockHomeAssistant,
|
||||||
data: AreaRegistryEntry[] = []
|
data: AreaRegistryEntry[] = []
|
||||||
) => hass.mockWS("config/area_registry/list", () => data);
|
) => {
|
||||||
|
hass.mockWS("config/area_registry/list", () => data);
|
||||||
|
const areas = {};
|
||||||
|
data.forEach((area) => {
|
||||||
|
areas[area.area_id] = area;
|
||||||
|
});
|
||||||
|
hass.updateHass({ areas });
|
||||||
|
};
|
||||||
|
@@ -10,6 +10,7 @@ export const mockConfigEntries = (hass: MockHomeAssistant) => {
|
|||||||
supports_options: false,
|
supports_options: false,
|
||||||
supports_remove_device: false,
|
supports_remove_device: false,
|
||||||
supports_unload: true,
|
supports_unload: true,
|
||||||
|
supports_reconfigure: true,
|
||||||
pref_disable_new_entities: false,
|
pref_disable_new_entities: false,
|
||||||
pref_disable_polling: false,
|
pref_disable_polling: false,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
|
@@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|||||||
export const mockDeviceRegistry = (
|
export const mockDeviceRegistry = (
|
||||||
hass: MockHomeAssistant,
|
hass: MockHomeAssistant,
|
||||||
data: DeviceRegistryEntry[] = []
|
data: DeviceRegistryEntry[] = []
|
||||||
) => hass.mockWS("config/device_registry/list", () => data);
|
) => {
|
||||||
|
hass.mockWS("config/device_registry/list", () => data);
|
||||||
|
const devices = {};
|
||||||
|
data.forEach((device) => {
|
||||||
|
devices[device.id] = device;
|
||||||
|
});
|
||||||
|
hass.updateHass({ devices });
|
||||||
|
};
|
||||||
|
7
demo/src/stubs/floor_registry.ts
Normal file
7
demo/src/stubs/floor_registry.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { FloorRegistryEntry } from "../../../src/data/floor_registry";
|
||||||
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
export const mockFloorRegistry = (
|
||||||
|
hass: MockHomeAssistant,
|
||||||
|
data: FloorRegistryEntry[] = []
|
||||||
|
) => hass.mockWS("config/floor_registry/list", () => data);
|
7
demo/src/stubs/label_registry.ts
Normal file
7
demo/src/stubs/label_registry.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||||
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
export const mockLabelRegistry = (
|
||||||
|
hass: MockHomeAssistant,
|
||||||
|
data: LabelRegistryEntry[] = []
|
||||||
|
) => hass.mockWS("config/label_registry/list", () => data);
|
@@ -17,6 +17,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
{
|
{
|
||||||
path: "trigger/0",
|
path: "trigger/0",
|
||||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
changed_variables: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"condition/0": [
|
"condition/0": [
|
||||||
|
@@ -17,6 +17,7 @@ export const motionLightTrace: DemoTrace = {
|
|||||||
{
|
{
|
||||||
path: "trigger/0",
|
path: "trigger/0",
|
||||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||||
|
changed_variables: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"action/0": [
|
"action/0": [
|
||||||
|
@@ -21,10 +21,10 @@ const ENTITIES = [
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const conditions = [
|
const conditions: Condition[] = [
|
||||||
{ condition: "and" },
|
{ condition: "and", conditions: [] },
|
||||||
{ condition: "not" },
|
{ condition: "not", conditions: [] },
|
||||||
{ condition: "or" },
|
{ condition: "or", conditions: [] },
|
||||||
{ condition: "state", entity_id: "light.kitchen", state: "on" },
|
{ condition: "state", entity_id: "light.kitchen", state: "on" },
|
||||||
{
|
{
|
||||||
condition: "numeric_state",
|
condition: "numeric_state",
|
||||||
@@ -34,11 +34,11 @@ const conditions = [
|
|||||||
above: 20,
|
above: 20,
|
||||||
},
|
},
|
||||||
{ condition: "sun", after: "sunset" },
|
{ condition: "sun", after: "sunset" },
|
||||||
{ condition: "sun", after: "sunrise", offset: "-01:00" },
|
{ condition: "sun", after: "sunrise", before_offset: 3600 },
|
||||||
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
|
{ condition: "zone", entity_id: "device_tracker.person", zone: "zone.home" },
|
||||||
{ condition: "trigger", id: "motion" },
|
{ condition: "trigger", id: "motion" },
|
||||||
{ condition: "time" },
|
{ condition: "time" },
|
||||||
{ condition: "template" },
|
{ condition: "template", value_template: "" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialCondition: Condition = {
|
const initialCondition: Condition = {
|
||||||
|
@@ -55,6 +55,7 @@ export class DemoAutomationTraceTimeline extends LitElement {
|
|||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
const hass = provideHass(this);
|
const hass = provideHass(this);
|
||||||
hass.updateTranslations(null, "en");
|
hass.updateTranslations(null, "en");
|
||||||
|
hass.updateTranslations("config", "en");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@@ -60,6 +60,7 @@ export class DemoAutomationTrace extends LitElement {
|
|||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
const hass = provideHass(this);
|
const hass = provideHass(this);
|
||||||
hass.updateTranslations(null, "en");
|
hass.updateTranslations(null, "en");
|
||||||
|
hass.updateTranslations("config", "en");
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles() {
|
static get styles() {
|
||||||
|
@@ -162,7 +162,7 @@ export class DemoHaBarButton extends LitElement {
|
|||||||
}
|
}
|
||||||
.custom-group {
|
.custom-group {
|
||||||
--control-button-group-thickness: 100px;
|
--control-button-group-thickness: 100px;
|
||||||
--control-button-group-border-radius: 18px;
|
--control-button-group-border-radius: 36px;
|
||||||
--control-button-group-spacing: 20px;
|
--control-button-group-spacing: 20px;
|
||||||
}
|
}
|
||||||
.custom-group ha-control-button {
|
.custom-group ha-control-button {
|
||||||
|
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
|
|||||||
--control-number-buttons-background-color: #2196f3;
|
--control-number-buttons-background-color: #2196f3;
|
||||||
--control-number-buttons-background-opacity: 0.1;
|
--control-number-buttons-background-opacity: 0.1;
|
||||||
--control-number-buttons-thickness: 100px;
|
--control-number-buttons-thickness: 100px;
|
||||||
--control-number-buttons-border-radius: 24px;
|
--control-number-buttons-border-radius: 36px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -186,8 +186,8 @@ export class DemoHaControlSelect extends LitElement {
|
|||||||
.custom {
|
.custom {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
--control-select-color: var(--state-fan-active-color);
|
--control-select-color: var(--state-fan-active-color);
|
||||||
--control-select-thickness: 100px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 24px;
|
--control-select-border-radius: 36px;
|
||||||
}
|
}
|
||||||
.vertical-selects {
|
.vertical-selects {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -150,8 +150,8 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
--control-slider-color: #ffcf4c;
|
--control-slider-color: #ffcf4c;
|
||||||
--control-slider-background: #ffcf4c;
|
--control-slider-background: #ffcf4c;
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
--control-slider-thickness: 100px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 24px;
|
--control-slider-border-radius: 36px;
|
||||||
}
|
}
|
||||||
.vertical-sliders {
|
.vertical-sliders {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -117,8 +117,8 @@ export class DemoHaControlSwitch extends LitElement {
|
|||||||
.custom {
|
.custom {
|
||||||
--control-switch-on-color: var(--green-color);
|
--control-switch-on-color: var(--green-color);
|
||||||
--control-switch-off-color: var(--red-color);
|
--control-switch-off-color: var(--red-color);
|
||||||
--control-switch-thickness: 100px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 24px;
|
--control-switch-border-radius: 36px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
@@ -59,6 +59,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@@ -77,6 +78,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@@ -95,30 +97,37 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const AREAS: AreaRegistryEntry[] = [
|
const AREAS: AreaRegistryEntry[] = [
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
|
floor_id: null,
|
||||||
name: "Backyard",
|
name: "Backyard",
|
||||||
icon: null,
|
icon: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
|
floor_id: null,
|
||||||
name: "Bedroom",
|
name: "Bedroom",
|
||||||
icon: "mdi:bed",
|
icon: "mdi:bed",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "livingroom",
|
area_id: "livingroom",
|
||||||
|
floor_id: null,
|
||||||
name: "Livingroom",
|
name: "Livingroom",
|
||||||
icon: "mdi:sofa",
|
icon: "mdi:sofa",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -17,6 +17,10 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
|
|||||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/demo-black-white-row";
|
import "../../components/demo-black-white-row";
|
||||||
|
import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||||
|
import { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||||
|
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||||
|
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||||
@@ -55,6 +59,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@@ -73,6 +78,7 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@@ -91,30 +97,76 @@ const DEVICES = [
|
|||||||
hw_version: null,
|
hw_version: null,
|
||||||
via_device_id: null,
|
via_device_id: null,
|
||||||
serial_number: null,
|
serial_number: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const AREAS: AreaRegistryEntry[] = [
|
const AREAS: AreaRegistryEntry[] = [
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
|
floor_id: "ground",
|
||||||
name: "Backyard",
|
name: "Backyard",
|
||||||
icon: null,
|
icon: null,
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "bedroom",
|
area_id: "bedroom",
|
||||||
|
floor_id: "first",
|
||||||
name: "Bedroom",
|
name: "Bedroom",
|
||||||
icon: "mdi:bed",
|
icon: "mdi:bed",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "livingroom",
|
area_id: "livingroom",
|
||||||
|
floor_id: "ground",
|
||||||
name: "Livingroom",
|
name: "Livingroom",
|
||||||
icon: "mdi:sofa",
|
icon: "mdi:sofa",
|
||||||
picture: null,
|
picture: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const FLOORS: FloorRegistryEntry[] = [
|
||||||
|
{
|
||||||
|
floor_id: "ground",
|
||||||
|
name: "Ground floor",
|
||||||
|
level: 0,
|
||||||
|
icon: null,
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
floor_id: "first",
|
||||||
|
name: "First floor",
|
||||||
|
level: 1,
|
||||||
|
icon: "mdi:numeric-1",
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
floor_id: "second",
|
||||||
|
name: "Second floor",
|
||||||
|
level: 2,
|
||||||
|
icon: "mdi:numeric-2",
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const LABELS: LabelRegistryEntry[] = [
|
||||||
|
{
|
||||||
|
label_id: "energy",
|
||||||
|
name: "Energy",
|
||||||
|
icon: null,
|
||||||
|
color: "yellow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label_id: "entertainment",
|
||||||
|
name: "Entertainment",
|
||||||
|
icon: "mdi:popcorn",
|
||||||
|
color: "blue",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -125,7 +177,12 @@ const SCHEMAS: {
|
|||||||
{
|
{
|
||||||
name: "One of each",
|
name: "One of each",
|
||||||
input: {
|
input: {
|
||||||
|
label: { name: "Label", selector: { label: {} } },
|
||||||
|
floor: { name: "Floor", selector: { floor: {} } },
|
||||||
|
area: { name: "Area", selector: { area: {} } },
|
||||||
|
device: { name: "Device", selector: { device: {} } },
|
||||||
entity: { name: "Entity", selector: { entity: {} } },
|
entity: { name: "Entity", selector: { entity: {} } },
|
||||||
|
target: { name: "Target", selector: { target: {} } },
|
||||||
state: {
|
state: {
|
||||||
name: "State",
|
name: "State",
|
||||||
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
|
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
|
||||||
@@ -134,15 +191,12 @@ const SCHEMAS: {
|
|||||||
name: "Attribute",
|
name: "Attribute",
|
||||||
selector: { attribute: { entity_id: "" } },
|
selector: { attribute: { entity_id: "" } },
|
||||||
},
|
},
|
||||||
device: { name: "Device", selector: { device: {} } },
|
|
||||||
config_entry: {
|
config_entry: {
|
||||||
name: "Integration",
|
name: "Integration",
|
||||||
selector: { config_entry: {} },
|
selector: { config_entry: {} },
|
||||||
},
|
},
|
||||||
duration: { name: "Duration", selector: { duration: {} } },
|
duration: { name: "Duration", selector: { duration: {} } },
|
||||||
addon: { name: "Addon", selector: { addon: {} } },
|
addon: { name: "Addon", selector: { addon: {} } },
|
||||||
area: { name: "Area", selector: { area: {} } },
|
|
||||||
target: { name: "Target", selector: { target: {} } },
|
|
||||||
number_box: {
|
number_box: {
|
||||||
name: "Number Box",
|
name: "Number Box",
|
||||||
selector: {
|
selector: {
|
||||||
@@ -291,6 +345,8 @@ const SCHEMAS: {
|
|||||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||||
|
floor: { name: "Floor", selector: { floor: { multiple: true } } },
|
||||||
|
label: { name: "Label", selector: { label: { multiple: true } } },
|
||||||
select: {
|
select: {
|
||||||
name: "Select Multiple",
|
name: "Select Multiple",
|
||||||
selector: {
|
selector: {
|
||||||
@@ -347,6 +403,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
|||||||
mockDeviceRegistry(hass, DEVICES);
|
mockDeviceRegistry(hass, DEVICES);
|
||||||
mockConfigEntries(hass);
|
mockConfigEntries(hass);
|
||||||
mockAreaRegistry(hass, AREAS);
|
mockAreaRegistry(hass, AREAS);
|
||||||
|
mockFloorRegistry(hass, FLOORS);
|
||||||
|
mockLabelRegistry(hass, LABELS);
|
||||||
mockHassioSupervisor(hass);
|
mockHassioSupervisor(hass);
|
||||||
hass.mockWS("auth/sign_path", (params) => params);
|
hass.mockWS("auth/sign_path", (params) => params);
|
||||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||||
|
@@ -11,7 +11,7 @@ const ENTITIES = [
|
|||||||
latitude: 32.877105,
|
latitude: 32.877105,
|
||||||
longitude: 117.232185,
|
longitude: 117.232185,
|
||||||
gps_accuracy: 91,
|
gps_accuracy: 91,
|
||||||
battery: 71,
|
battery: 25,
|
||||||
friendly_name: "Paulus",
|
friendly_name: "Paulus",
|
||||||
}),
|
}),
|
||||||
getEntity("device_tracker", "demo_anne_therese", "school", {
|
getEntity("device_tracker", "demo_anne_therese", "school", {
|
||||||
@@ -19,7 +19,7 @@ const ENTITIES = [
|
|||||||
latitude: 32.877105,
|
latitude: 32.877105,
|
||||||
longitude: 117.232185,
|
longitude: 117.232185,
|
||||||
gps_accuracy: 91,
|
gps_accuracy: 91,
|
||||||
battery: 71,
|
battery: 50,
|
||||||
friendly_name: "Anne Therese",
|
friendly_name: "Anne Therese",
|
||||||
}),
|
}),
|
||||||
getEntity("device_tracker", "demo_home_boy", "home", {
|
getEntity("device_tracker", "demo_home_boy", "home", {
|
||||||
@@ -27,7 +27,7 @@ const ENTITIES = [
|
|||||||
latitude: 32.877105,
|
latitude: 32.877105,
|
||||||
longitude: 117.232185,
|
longitude: 117.232185,
|
||||||
gps_accuracy: 91,
|
gps_accuracy: 91,
|
||||||
battery: 71,
|
battery: 75,
|
||||||
friendly_name: "Home Boy",
|
friendly_name: "Home Boy",
|
||||||
}),
|
}),
|
||||||
getEntity("light", "bed_light", "on", {
|
getEntity("light", "bed_light", "on", {
|
||||||
@@ -39,21 +39,53 @@ const ENTITIES = [
|
|||||||
getEntity("light", "ceiling_lights", "off", {
|
getEntity("light", "ceiling_lights", "off", {
|
||||||
friendly_name: "Ceiling Lights",
|
friendly_name: "Ceiling Lights",
|
||||||
}),
|
}),
|
||||||
|
getEntity("sensor", "battery_1", 20, {
|
||||||
|
device_class: "battery",
|
||||||
|
friendly_name: "Battery 1",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
}),
|
||||||
|
getEntity("sensor", "battery_2", 35, {
|
||||||
|
device_class: "battery",
|
||||||
|
friendly_name: "Battery 2",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
}),
|
||||||
|
getEntity("sensor", "battery_3", 40, {
|
||||||
|
device_class: "battery",
|
||||||
|
friendly_name: "Battery 3",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
}),
|
||||||
|
getEntity("sensor", "battery_4", 80, {
|
||||||
|
device_class: "battery",
|
||||||
|
friendly_name: "Battery 4",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
}),
|
||||||
|
getEntity("input_number", "min_battery_level", 30, {
|
||||||
|
mode: "slider",
|
||||||
|
step: 10,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
icon: "mdi:battery-alert-variant",
|
||||||
|
friendly_name: "Minimum Battery Level",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
heading: "Unfiltered controller",
|
heading: "Unfiltered entities",
|
||||||
config: `
|
config: `
|
||||||
- type: entities
|
- type: entities
|
||||||
entities:
|
entities:
|
||||||
|
- device_tracker.demo_anne_therese
|
||||||
|
- device_tracker.demo_home_boy
|
||||||
|
- device_tracker.demo_paulus
|
||||||
- light.bed_light
|
- light.bed_light
|
||||||
- light.ceiling_lights
|
- light.ceiling_lights
|
||||||
- light.kitchen_lights
|
- light.kitchen_lights
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Filtered entities card",
|
heading: "On and home entities",
|
||||||
config: `
|
config: `
|
||||||
- type: entity-filter
|
- type: entity-filter
|
||||||
entities:
|
entities:
|
||||||
@@ -63,11 +95,30 @@ const CONFIGS = [
|
|||||||
- light.bed_light
|
- light.bed_light
|
||||||
- light.ceiling_lights
|
- light.ceiling_lights
|
||||||
- light.kitchen_lights
|
- light.kitchen_lights
|
||||||
state_filter:
|
conditions:
|
||||||
|
- condition: state
|
||||||
|
state:
|
||||||
- "on"
|
- "on"
|
||||||
- home
|
- home
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
heading: "Same state as Bed Light",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- device_tracker.demo_anne_therese
|
||||||
|
- device_tracker.demo_home_boy
|
||||||
|
- device_tracker.demo_paulus
|
||||||
|
- light.bed_light
|
||||||
|
- light.ceiling_lights
|
||||||
|
- light.kitchen_lights
|
||||||
|
conditions:
|
||||||
|
- condition: state
|
||||||
|
state:
|
||||||
|
- light.bed_light
|
||||||
|
`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
heading: 'With "entities" card config',
|
heading: 'With "entities" card config',
|
||||||
config: `
|
config: `
|
||||||
@@ -79,9 +130,11 @@ const CONFIGS = [
|
|||||||
- light.bed_light
|
- light.bed_light
|
||||||
- light.ceiling_lights
|
- light.ceiling_lights
|
||||||
- light.kitchen_lights
|
- light.kitchen_lights
|
||||||
state_filter:
|
conditions:
|
||||||
|
- condition: state
|
||||||
|
state:
|
||||||
- "on"
|
- "on"
|
||||||
- not_home
|
- home
|
||||||
card:
|
card:
|
||||||
type: entities
|
type: entities
|
||||||
title: Custom Title
|
title: Custom Title
|
||||||
@@ -99,15 +152,101 @@ const CONFIGS = [
|
|||||||
- light.bed_light
|
- light.bed_light
|
||||||
- light.ceiling_lights
|
- light.ceiling_lights
|
||||||
- light.kitchen_lights
|
- light.kitchen_lights
|
||||||
state_filter:
|
conditions:
|
||||||
|
- condition: state
|
||||||
|
state:
|
||||||
- "on"
|
- "on"
|
||||||
- not_home
|
- home
|
||||||
card:
|
card:
|
||||||
type: glance
|
type: glance
|
||||||
show_state: true
|
show_state: true
|
||||||
title: Custom Title
|
title: Custom Title
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
heading:
|
||||||
|
"Filtered entities by battery attribute (< '30') using state filter",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- device_tracker.demo_anne_therese
|
||||||
|
- device_tracker.demo_home_boy
|
||||||
|
- device_tracker.demo_paulus
|
||||||
|
state_filter:
|
||||||
|
- operator: <
|
||||||
|
attribute: battery
|
||||||
|
value: "30"
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Unfiltered number entities",
|
||||||
|
config: `
|
||||||
|
- type: entities
|
||||||
|
entities:
|
||||||
|
- input_number.min_battery_level
|
||||||
|
- sensor.battery_1
|
||||||
|
- sensor.battery_3
|
||||||
|
- sensor.battery_2
|
||||||
|
- sensor.battery_4
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Battery lower than 50%",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- sensor.battery_1
|
||||||
|
- sensor.battery_3
|
||||||
|
- sensor.battery_2
|
||||||
|
- sensor.battery_4
|
||||||
|
conditions:
|
||||||
|
- condition: numeric_state
|
||||||
|
below: 50
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Battery lower than min battery level",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- sensor.battery_1
|
||||||
|
- sensor.battery_3
|
||||||
|
- sensor.battery_2
|
||||||
|
- sensor.battery_4
|
||||||
|
conditions:
|
||||||
|
- condition: numeric_state
|
||||||
|
below: input_number.min_battery_level
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Battery between min battery level and 70%",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- sensor.battery_1
|
||||||
|
- sensor.battery_3
|
||||||
|
- sensor.battery_2
|
||||||
|
- sensor.battery_4
|
||||||
|
conditions:
|
||||||
|
- condition: numeric_state
|
||||||
|
above: input_number.min_battery_level
|
||||||
|
below: 70
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Error: Entities must be specified",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Error: Incorrect filter config",
|
||||||
|
config: `
|
||||||
|
- type: entity-filter
|
||||||
|
entities:
|
||||||
|
- sensor.gas_station_lowest_price
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@customElement("demo-lovelace-entity-filter-card")
|
@customElement("demo-lovelace-entity-filter-card")
|
||||||
|
@@ -36,6 +36,45 @@ const ENTITIES = [
|
|||||||
friendly_name: "Nest",
|
friendly_name: "Nest",
|
||||||
supported_features: 43,
|
supported_features: 43,
|
||||||
}),
|
}),
|
||||||
|
getEntity("climate", "overkiz_radiator", "heat", {
|
||||||
|
current_temperature: 18,
|
||||||
|
min_temp: 7,
|
||||||
|
max_temp: 35,
|
||||||
|
temperature: 20,
|
||||||
|
hvac_modes: ["heat", "auto", "off"],
|
||||||
|
friendly_name: "Overkiz radiator",
|
||||||
|
supported_features: 17,
|
||||||
|
preset_mode: "comfort",
|
||||||
|
preset_modes: [
|
||||||
|
"none",
|
||||||
|
"frost_protection",
|
||||||
|
"eco",
|
||||||
|
"comfort",
|
||||||
|
"comfort-1",
|
||||||
|
"comfort-2",
|
||||||
|
"auto",
|
||||||
|
"boost",
|
||||||
|
"external",
|
||||||
|
"prog",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
getEntity("climate", "overkiz_towel_dryer", "heat", {
|
||||||
|
current_temperature: null,
|
||||||
|
min_temp: 7,
|
||||||
|
max_temp: 35,
|
||||||
|
hvac_modes: ["heat", "off"],
|
||||||
|
friendly_name: "Overkiz towel dryer",
|
||||||
|
supported_features: 16,
|
||||||
|
preset_mode: "eco",
|
||||||
|
preset_modes: [
|
||||||
|
"none",
|
||||||
|
"frost_protection",
|
||||||
|
"eco",
|
||||||
|
"comfort",
|
||||||
|
"comfort-1",
|
||||||
|
"comfort-2",
|
||||||
|
],
|
||||||
|
}),
|
||||||
getEntity("climate", "sensibo", "fan_only", {
|
getEntity("climate", "sensibo", "fan_only", {
|
||||||
current_temperature: null,
|
current_temperature: null,
|
||||||
temperature: null,
|
temperature: null,
|
||||||
@@ -46,7 +85,9 @@ const ENTITIES = [
|
|||||||
friendly_name: "Sensibo purifier",
|
friendly_name: "Sensibo purifier",
|
||||||
fan_modes: ["low", "high"],
|
fan_modes: ["low", "high"],
|
||||||
fan_mode: "low",
|
fan_mode: "low",
|
||||||
supported_features: 9,
|
swing_modes: ["on", "off", "both", "vertical", "horizontal"],
|
||||||
|
swing_mode: "vertical",
|
||||||
|
supported_features: 41,
|
||||||
}),
|
}),
|
||||||
getEntity("climate", "unavailable", "unavailable", {
|
getEntity("climate", "unavailable", "unavailable", {
|
||||||
supported_features: 43,
|
supported_features: 43,
|
||||||
@@ -59,8 +100,6 @@ const CONFIGS = [
|
|||||||
config: `
|
config: `
|
||||||
- type: thermostat
|
- type: thermostat
|
||||||
entity: climate.ecobee
|
entity: climate.ecobee
|
||||||
- type: thermostat
|
|
||||||
entity: climate.nest
|
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,6 +109,66 @@ const CONFIGS = [
|
|||||||
entity: climate.nest
|
entity: climate.nest
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
heading: "Feature example",
|
||||||
|
config: `
|
||||||
|
- type: thermostat
|
||||||
|
entity: climate.overkiz_radiator
|
||||||
|
features:
|
||||||
|
- type: climate-hvac-modes
|
||||||
|
hvac_modes:
|
||||||
|
- heat
|
||||||
|
- 'off'
|
||||||
|
- auto
|
||||||
|
- type: climate-preset-modes
|
||||||
|
style: icons
|
||||||
|
preset_modes:
|
||||||
|
- none
|
||||||
|
- frost_protection
|
||||||
|
- eco
|
||||||
|
- comfort
|
||||||
|
- comfort-1
|
||||||
|
- comfort-2
|
||||||
|
- auto
|
||||||
|
- boost
|
||||||
|
- external
|
||||||
|
- prog
|
||||||
|
- type: climate-preset-modes
|
||||||
|
style: dropdown
|
||||||
|
preset_modes:
|
||||||
|
- none
|
||||||
|
- frost_protection
|
||||||
|
- eco
|
||||||
|
- comfort
|
||||||
|
- comfort-1
|
||||||
|
- comfort-2
|
||||||
|
- auto
|
||||||
|
- boost
|
||||||
|
- external
|
||||||
|
- prog
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Preset only example",
|
||||||
|
config: `
|
||||||
|
- type: thermostat
|
||||||
|
entity: climate.overkiz_towel_dryer
|
||||||
|
features:
|
||||||
|
- type: climate-hvac-modes
|
||||||
|
hvac_modes:
|
||||||
|
- heat
|
||||||
|
- 'off'
|
||||||
|
- type: climate-preset-modes
|
||||||
|
style: icons
|
||||||
|
preset_modes:
|
||||||
|
- none
|
||||||
|
- frost_protection
|
||||||
|
- eco
|
||||||
|
- comfort
|
||||||
|
- comfort-1
|
||||||
|
- comfort-2
|
||||||
|
`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
heading: "Fan only example",
|
heading: "Fan only example",
|
||||||
config: `
|
config: `
|
||||||
@@ -85,6 +184,14 @@ const CONFIGS = [
|
|||||||
fan_modes:
|
fan_modes:
|
||||||
- low
|
- low
|
||||||
- high
|
- high
|
||||||
|
- type: climate-swing-modes
|
||||||
|
style: icons
|
||||||
|
swing_modes:
|
||||||
|
- 'on'
|
||||||
|
- 'off'
|
||||||
|
- 'both'
|
||||||
|
- 'vertical'
|
||||||
|
- 'horizontal'
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -406,6 +406,7 @@ export class DemoEntityState extends LitElement {
|
|||||||
entity_id: "select.speed",
|
entity_id: "select.speed",
|
||||||
translation_key: "speed",
|
translation_key: "speed",
|
||||||
platform: "demo",
|
platform: "demo",
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -31,6 +31,7 @@ const createConfigEntry = (
|
|||||||
supports_options: false,
|
supports_options: false,
|
||||||
supports_remove_device: false,
|
supports_remove_device: false,
|
||||||
supports_unload: true,
|
supports_unload: true,
|
||||||
|
supports_reconfigure: true,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
pref_disable_new_entities: false,
|
pref_disable_new_entities: false,
|
||||||
pref_disable_polling: false,
|
pref_disable_polling: false,
|
||||||
@@ -198,6 +199,8 @@ const createEntityRegistryEntries = (
|
|||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
unique_id: "updater",
|
unique_id: "updater",
|
||||||
options: null,
|
options: null,
|
||||||
|
labels: [],
|
||||||
|
categories: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -221,6 +224,7 @@ const createDeviceRegistryEntries = (
|
|||||||
name_by_user: null,
|
name_by_user: null,
|
||||||
disabled_by: null,
|
disabled_by: null,
|
||||||
configuration_url: null,
|
configuration_url: null,
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import "../../components/demo-more-infos";
|
|||||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("climate", "thermostat", "heat", {
|
getEntity("climate", "radiator", "heat", {
|
||||||
friendly_name: "Basic heater",
|
friendly_name: "Basic heater",
|
||||||
hvac_modes: ["heat", "off"],
|
hvac_modes: ["heat", "off"],
|
||||||
hvac_mode: "heat",
|
hvac_mode: "heat",
|
||||||
@@ -80,6 +80,24 @@ const ENTITIES = [
|
|||||||
max_humidity: 100,
|
max_humidity: 100,
|
||||||
humidity: 50,
|
humidity: 50,
|
||||||
}),
|
}),
|
||||||
|
getEntity("climate", "towel_dryer", "heat", {
|
||||||
|
friendly_name: "Preset only heater",
|
||||||
|
hvac_modes: ["heat", "off"],
|
||||||
|
hvac_mode: "heat",
|
||||||
|
preset_modes: [
|
||||||
|
"none",
|
||||||
|
"frost_protection",
|
||||||
|
"eco",
|
||||||
|
"comfort",
|
||||||
|
"comfort-1",
|
||||||
|
"comfort-2",
|
||||||
|
],
|
||||||
|
preset_mode: "eco",
|
||||||
|
current_temperature: null,
|
||||||
|
min_temp: 7,
|
||||||
|
max_temp: 35,
|
||||||
|
supported_features: ClimateEntityFeature.PRESET_MODE,
|
||||||
|
}),
|
||||||
getEntity("climate", "unavailable", "unavailable", {
|
getEntity("climate", "unavailable", "unavailable", {
|
||||||
friendly_name: "Unavailable heater",
|
friendly_name: "Unavailable heater",
|
||||||
hvac_modes: ["heat", "off"],
|
hvac_modes: ["heat", "off"],
|
||||||
|
80
package.json
80
package.json
@@ -25,22 +25,22 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.23.9",
|
"@babel/runtime": "7.24.1",
|
||||||
"@braintree/sanitize-url": "7.0.0",
|
"@braintree/sanitize-url": "7.0.1",
|
||||||
"@codemirror/autocomplete": "6.12.0",
|
"@codemirror/autocomplete": "6.15.0",
|
||||||
"@codemirror/commands": "6.3.3",
|
"@codemirror/commands": "6.3.3",
|
||||||
"@codemirror/language": "6.10.1",
|
"@codemirror/language": "6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.3.3",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/search": "6.5.6",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "6.24.1",
|
"@codemirror/view": "6.26.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.12.2",
|
"@formatjs/intl-datetimeformat": "6.12.3",
|
||||||
"@formatjs/intl-displaynames": "6.6.6",
|
"@formatjs/intl-displaynames": "6.6.6",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||||
"@formatjs/intl-listformat": "7.5.5",
|
"@formatjs/intl-listformat": "7.5.5",
|
||||||
"@formatjs/intl-locale": "3.4.5",
|
"@formatjs/intl-locale": "3.4.5",
|
||||||
"@formatjs/intl-numberformat": "8.10.0",
|
"@formatjs/intl-numberformat": "8.10.1",
|
||||||
"@formatjs/intl-pluralrules": "5.2.12",
|
"@formatjs/intl-pluralrules": "5.2.12",
|
||||||
"@formatjs/intl-relativetimeformat": "11.2.12",
|
"@formatjs/intl-relativetimeformat": "11.2.12",
|
||||||
"@fullcalendar/core": "6.1.11",
|
"@fullcalendar/core": "6.1.11",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"@lit-labs/motion": "1.0.7",
|
"@lit-labs/motion": "1.0.7",
|
||||||
"@lit-labs/observers": "2.0.2",
|
"@lit-labs/observers": "2.0.2",
|
||||||
"@lit-labs/virtualizer": "2.0.12",
|
"@lit-labs/virtualizer": "2.0.12",
|
||||||
"@lrnwebcomponents/simple-tooltip": "patch:@lrnwebcomponents/simple-tooltip@npm%3A8.0.0#~/.yarn/patches/@lrnwebcomponents-simple-tooltip-npm-8.0.0-77591f2e0c.patch",
|
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/mwc-base": "0.27.0",
|
"@material/mwc-base": "0.27.0",
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
"@material/mwc-radio": "0.27.0",
|
"@material/mwc-radio": "0.27.0",
|
||||||
"@material/mwc-ripple": "0.27.0",
|
"@material/mwc-ripple": "0.27.0",
|
||||||
"@material/mwc-select": "0.27.0",
|
"@material/mwc-select": "0.27.0",
|
||||||
|
"@material/mwc-snackbar": "0.27.0",
|
||||||
"@material/mwc-switch": "0.27.0",
|
"@material/mwc-switch": "0.27.0",
|
||||||
"@material/mwc-tab": "0.27.0",
|
"@material/mwc-tab": "0.27.0",
|
||||||
"@material/mwc-tab-bar": "0.27.0",
|
"@material/mwc-tab-bar": "0.27.0",
|
||||||
@@ -86,11 +87,10 @@
|
|||||||
"@polymer/paper-item": "3.0.1",
|
"@polymer/paper-item": "3.0.1",
|
||||||
"@polymer/paper-listbox": "3.0.1",
|
"@polymer/paper-listbox": "3.0.1",
|
||||||
"@polymer/paper-tabs": "3.1.0",
|
"@polymer/paper-tabs": "3.1.0",
|
||||||
"@polymer/paper-toast": "3.0.1",
|
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.3.6",
|
"@vaadin/combo-box": "24.3.10",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.3.6",
|
"@vaadin/vaadin-themable-mixin": "24.3.10",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@@ -98,20 +98,20 @@
|
|||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"chart.js": "4.4.1",
|
"chart.js": "4.4.2",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.0",
|
||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.36.0",
|
"core-js": "3.36.1",
|
||||||
"cropperjs": "1.6.1",
|
"cropperjs": "1.6.1",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"date-fns-tz": "2.0.0",
|
"date-fns-tz": "2.0.1",
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"element-internals-polyfill": "1.3.10",
|
"element-internals-polyfill": "1.3.10",
|
||||||
"fuse.js": "7.0.0",
|
"fuse.js": "7.0.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
"home-assistant-js-websocket": "9.1.0",
|
"home-assistant-js-websocket": "9.2.1",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.5.11",
|
"intl-messageformat": "10.5.11",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
"lit": "2.8.0",
|
"lit": "2.8.0",
|
||||||
"luxon": "3.4.4",
|
"luxon": "3.4.4",
|
||||||
"marked": "12.0.0",
|
"marked": "12.0.1",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "0.3.2",
|
"proxy-polyfill": "0.3.2",
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
"rrule": "2.8.1",
|
"rrule": "2.8.1",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "1.0.3",
|
"superstruct": "1.0.4",
|
||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"tsparticles-engine": "2.12.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.12.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
@@ -147,20 +147,20 @@
|
|||||||
"workbox-precaching": "7.0.0",
|
"workbox-precaching": "7.0.0",
|
||||||
"workbox-routing": "7.0.0",
|
"workbox-routing": "7.0.0",
|
||||||
"workbox-strategies": "7.0.0",
|
"workbox-strategies": "7.0.0",
|
||||||
"xss": "1.0.14"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.23.9",
|
"@babel/core": "7.24.3",
|
||||||
"@babel/helper-define-polyfill-provider": "0.5.0",
|
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.23.9",
|
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||||
"@babel/plugin-transform-runtime": "7.23.9",
|
"@babel/plugin-transform-runtime": "7.24.3",
|
||||||
"@babel/preset-env": "7.23.9",
|
"@babel/preset-env": "7.24.3",
|
||||||
"@babel/preset-typescript": "7.23.3",
|
"@babel/preset-typescript": "7.24.1",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.10.1",
|
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||||
"@koa/cors": "5.0.0",
|
"@koa/cors": "5.0.0",
|
||||||
"@lokalise/node-api": "12.1.0",
|
"@lokalise/node-api": "12.3.0",
|
||||||
"@octokit/auth-oauth-device": "6.0.1",
|
"@octokit/auth-oauth-device": "7.0.1",
|
||||||
"@octokit/plugin-retry": "6.0.1",
|
"@octokit/plugin-retry": "7.0.3",
|
||||||
"@octokit/rest": "20.0.2",
|
"@octokit/rest": "20.0.2",
|
||||||
"@open-wc/dev-server-hmr": "0.1.4",
|
"@open-wc/dev-server-hmr": "0.1.4",
|
||||||
"@rollup/plugin-babel": "6.0.4",
|
"@rollup/plugin-babel": "6.0.4",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.13",
|
"@types/chromecast-caf-receiver": "6.0.13",
|
||||||
"@types/chromecast-caf-sender": "1.0.8",
|
"@types/chromecast-caf-sender": "1.0.9",
|
||||||
"@types/color-name": "1.1.3",
|
"@types/color-name": "1.1.3",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
@@ -185,8 +185,8 @@
|
|||||||
"@types/tar": "6.1.11",
|
"@types/tar": "6.1.11",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "7.0.2",
|
"@typescript-eslint/eslint-plugin": "7.4.0",
|
||||||
"@typescript-eslint/parser": "7.0.2",
|
"@typescript-eslint/parser": "7.4.0",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
"del": "7.1.0",
|
"del": "7.1.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.1.0",
|
"eslint-config-airbnb-typescript": "18.0.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-webpack": "0.13.8",
|
"eslint-import-resolver-webpack": "0.13.8",
|
||||||
"eslint-plugin-disable": "2.0.3",
|
"eslint-plugin-disable": "2.0.3",
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
"gulp-flatmap": "1.0.2",
|
"gulp-flatmap": "1.0.2",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-merge-json": "2.1.2",
|
"gulp-merge-json": "2.2.1",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
"gulp-zopfli-green": "6.0.1",
|
"gulp-zopfli-green": "6.0.1",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
@@ -220,11 +220,11 @@
|
|||||||
"lint-staged": "15.2.2",
|
"lint-staged": "15.2.2",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"magic-string": "0.30.7",
|
"magic-string": "0.30.8",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"mocha": "10.3.0",
|
"mocha": "10.3.0",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
"open": "10.0.3",
|
"open": "10.1.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.2.5",
|
"prettier": "3.2.5",
|
||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
@@ -235,16 +235,16 @@
|
|||||||
"sinon": "17.0.1",
|
"sinon": "17.0.1",
|
||||||
"source-map-url": "0.4.1",
|
"source-map-url": "0.4.1",
|
||||||
"systemjs": "6.14.3",
|
"systemjs": "6.14.3",
|
||||||
"tar": "6.2.0",
|
"tar": "6.2.1",
|
||||||
"terser-webpack-plugin": "5.3.10",
|
"terser-webpack-plugin": "5.3.10",
|
||||||
"transform-async-modules-webpack-plugin": "1.0.2",
|
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.4.3",
|
||||||
"vinyl-buffer": "1.0.1",
|
"vinyl-buffer": "1.0.1",
|
||||||
"vinyl-source-stream": "2.0.0",
|
"vinyl-source-stream": "2.0.0",
|
||||||
"webpack": "5.90.3",
|
"webpack": "5.91.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "5.0.2",
|
"webpack-dev-server": "5.0.4",
|
||||||
"webpack-manifest-plugin": "5.0.0",
|
"webpack-manifest-plugin": "5.0.0",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "6.0.1",
|
"webpackbar": "6.0.1",
|
||||||
@@ -260,5 +260,5 @@
|
|||||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.1.0"
|
"packageManager": "yarn@4.1.1"
|
||||||
}
|
}
|
||||||
|
1
public/static/images/appstore.svg
Normal file
1
public/static/images/appstore.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.1 KiB |
BIN
public/static/images/logo_apple_home.png
Normal file
BIN
public/static/images/logo_apple_home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
BIN
public/static/images/logo_google_home.png
Normal file
BIN
public/static/images/logo_google_home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
1
public/static/images/playstore.svg
Normal file
1
public/static/images/playstore.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.2 KiB |
1
public/static/images/qr-appstore.svg
Normal file
1
public/static/images/qr-appstore.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 52 KiB |
1
public/static/images/qr-playstore.svg
Normal file
1
public/static/images/qr-playstore.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 69 KiB |
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20240306.0"
|
version = "20240403.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10.0"
|
requires-python = ">=3.11.0"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { theme2hex } from "./convert-color";
|
||||||
|
|
||||||
export const COLORS = [
|
export const COLORS = [
|
||||||
"#44739e",
|
"#44739e",
|
||||||
"#984ea3",
|
"#984ea3",
|
||||||
@@ -65,10 +67,10 @@ export function getColorByIndex(index: number) {
|
|||||||
export function getGraphColorByIndex(
|
export function getGraphColorByIndex(
|
||||||
index: number,
|
index: number,
|
||||||
style: CSSStyleDeclaration
|
style: CSSStyleDeclaration
|
||||||
) {
|
): string {
|
||||||
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
|
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
|
||||||
return (
|
const themeColor =
|
||||||
style.getPropertyValue(`--graph-color-${index + 1}`) ||
|
style.getPropertyValue(`--graph-color-${index + 1}`) ||
|
||||||
getColorByIndex(index)
|
getColorByIndex(index);
|
||||||
);
|
return theme2hex(themeColor);
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import colors from "color-name";
|
||||||
import { expandHex } from "./hex";
|
import { expandHex } from "./hex";
|
||||||
|
|
||||||
const rgb_hex = (component: number): string => {
|
const rgb_hex = (component: number): string => {
|
||||||
@@ -126,3 +127,18 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
|
|||||||
|
|
||||||
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||||
hsv2rgb([hs[0], hs[1], 255]);
|
hsv2rgb([hs[0], hs[1], 255]);
|
||||||
|
|
||||||
|
export function theme2hex(themeColor: string): string {
|
||||||
|
if (themeColor.startsWith("#")) {
|
||||||
|
return themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rgbFromColorName = colors[themeColor];
|
||||||
|
if (!rgbFromColorName) {
|
||||||
|
// We have a named color, and there's nothing in the table,
|
||||||
|
// so nothing further we can do with it.
|
||||||
|
// Compare/border/background color will all be the same.
|
||||||
|
return themeColor;
|
||||||
|
}
|
||||||
|
return rgb2hex(rgbFromColorName);
|
||||||
|
}
|
||||||
|
@@ -231,6 +231,7 @@ export const SENSOR_ENTITIES = [
|
|||||||
"calendar",
|
"calendar",
|
||||||
"camera",
|
"camera",
|
||||||
"device_tracker",
|
"device_tracker",
|
||||||
|
"image",
|
||||||
"weather",
|
"weather",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -37,3 +37,20 @@ export const calcDateProperty = (
|
|||||||
locale.time_zone === TimeZone.server
|
locale.time_zone === TimeZone.server
|
||||||
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
|
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
|
||||||
: fn(date, options);
|
: fn(date, options);
|
||||||
|
|
||||||
|
export const calcDateDifferenceProperty = (
|
||||||
|
endDate: Date,
|
||||||
|
startDate: Date,
|
||||||
|
fn: (date: Date, options?: any) => boolean | number,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig
|
||||||
|
) =>
|
||||||
|
calcDateProperty(
|
||||||
|
endDate,
|
||||||
|
fn,
|
||||||
|
locale,
|
||||||
|
config,
|
||||||
|
locale.time_zone === TimeZone.server
|
||||||
|
? utcToZonedTime(startDate, config.time_zone)
|
||||||
|
: startDate
|
||||||
|
);
|
||||||
|
@@ -16,6 +16,7 @@ import { customElement, property, state, query } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
numberFormatToLocale,
|
numberFormatToLocale,
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
getDisplayUnit,
|
getDisplayUnit,
|
||||||
getStatisticLabel,
|
getStatisticLabel,
|
||||||
getStatisticMetadata,
|
getStatisticMetadata,
|
||||||
|
isExternalStatistic,
|
||||||
Statistics,
|
Statistics,
|
||||||
statisticsHaveType,
|
statisticsHaveType,
|
||||||
StatisticsMetaData,
|
StatisticsMetaData,
|
||||||
@@ -79,6 +81,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public isLoadingData = false;
|
@property({ type: Boolean }) public isLoadingData = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public clickForMoreInfo = true;
|
||||||
|
|
||||||
@property() public period?: string;
|
@property() public period?: string;
|
||||||
|
|
||||||
@state() private _chartData: ChartData = { datasets: [] };
|
@state() private _chartData: ChartData = { datasets: [] };
|
||||||
@@ -273,6 +277,33 @@ export class StatisticsChart extends LitElement {
|
|||||||
},
|
},
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(this.hass.locale),
|
locale: numberFormatToLocale(this.hass.locale),
|
||||||
|
onClick: (e: any) => {
|
||||||
|
if (
|
||||||
|
!this.clickForMoreInfo ||
|
||||||
|
!(e.native instanceof MouseEvent) ||
|
||||||
|
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chart = e.chart;
|
||||||
|
|
||||||
|
const points = chart.getElementsAtEventForMode(
|
||||||
|
e,
|
||||||
|
"nearest",
|
||||||
|
{ intersect: true },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points.length) {
|
||||||
|
const firstPoint = points[0];
|
||||||
|
const statisticId = this._statisticIds[firstPoint.datasetIndex];
|
||||||
|
if (!isExternalStatistic(statisticId)) {
|
||||||
|
fireEvent(this, "hass-more-info", { entityId: statisticId });
|
||||||
|
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,22 +4,24 @@ import { css, html } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-assist-chip")
|
@customElement("ha-assist-chip")
|
||||||
|
// @ts-ignore
|
||||||
export class HaAssistChip extends MdAssistChip {
|
export class HaAssistChip extends MdAssistChip {
|
||||||
@property({ type: Boolean, reflect: true }) filled = false;
|
@property({ type: Boolean, reflect: true }) filled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) active = false;
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
...super.styles,
|
...super.styles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
--md-sys-color-primary: var(--primary-text-color);
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
--md-sys-color-on-surface: var(--primary-text-color);
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
--md-assist-chip-container-shape: 16px;
|
--md-assist-chip-container-shape: var(
|
||||||
|
--ha-assist-chip-container-shape,
|
||||||
|
16px
|
||||||
|
);
|
||||||
--md-assist-chip-outline-color: var(--outline-color);
|
--md-assist-chip-outline-color: var(--outline-color);
|
||||||
--md-assist-chip-label-text-weight: 400;
|
--md-assist-chip-label-text-weight: 400;
|
||||||
--ha-assist-chip-filled-container-color: rgba(
|
|
||||||
var(--rgb-primary-text-color),
|
|
||||||
0.15
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
||||||
.filled {
|
.filled {
|
||||||
@@ -31,10 +33,28 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
background-color: var(--ha-assist-chip-filled-container-color);
|
background-color: var(--ha-assist-chip-filled-container-color);
|
||||||
}
|
}
|
||||||
/** Set the size of mdc icons **/
|
/** Set the size of mdc icons **/
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]),
|
||||||
|
::slotted([slot="trailingIcon"]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trailing.icon ::slotted(*),
|
||||||
|
.trailing.icon svg {
|
||||||
|
margin-inline-end: unset;
|
||||||
|
margin-inline-start: var(--_icon-label-space);
|
||||||
|
}
|
||||||
|
::before {
|
||||||
|
background: var(--ha-assist-chip-container-color, transparent);
|
||||||
|
opacity: var(--ha-assist-chip-container-opacity, 1);
|
||||||
|
}
|
||||||
|
:where(.active)::before {
|
||||||
|
background: var(--ha-assist-chip-active-container-color);
|
||||||
|
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -45,6 +65,30 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
|
|
||||||
return super.renderOutline();
|
return super.renderOutline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override getContainerClasses() {
|
||||||
|
return {
|
||||||
|
...super.getContainerClasses(),
|
||||||
|
active: this.active,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override renderPrimaryContent() {
|
||||||
|
return html`
|
||||||
|
<span class="leading icon" aria-hidden="true">
|
||||||
|
${this.renderLeadingIcon()}
|
||||||
|
</span>
|
||||||
|
<span class="label">${this.label}</span>
|
||||||
|
<span class="touch"></span>
|
||||||
|
<span class="trailing leading icon" aria-hidden="true">
|
||||||
|
${this.renderTrailingIcon()}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderTrailingIcon() {
|
||||||
|
return html`<slot name="trailing-icon"></slot>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -19,12 +19,16 @@ export class HaInputChip extends MdInputChip {
|
|||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
|
--ha-input-chip-selected-container-opacity: 1;
|
||||||
}
|
}
|
||||||
/** Set the size of mdc icons **/
|
/** Set the size of mdc icons **/
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
}
|
}
|
||||||
|
.selected::before {
|
||||||
|
opacity: var(--ha-input-chip-selected-container-opacity);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
130
src/components/data-table/ha-data-table-labels.ts
Normal file
130
src/components/data-table/ha-data-table-labels.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { css, html, LitElement, nothing, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
|
import { computeCssColor } from "../../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import "../ha-label";
|
||||||
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
|
||||||
|
@customElement("ha-data-table-labels")
|
||||||
|
class HaDataTableLabels extends LitElement {
|
||||||
|
@property({ attribute: false }) public labels!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const labels = this.labels.sort((a, b) => stringCompare(a.name, b.name));
|
||||||
|
return html`
|
||||||
|
<ha-chip-set>
|
||||||
|
${repeat(
|
||||||
|
labels.slice(0, 2),
|
||||||
|
(label) => label.label_id,
|
||||||
|
(label) => this._renderLabel(label, true)
|
||||||
|
)}
|
||||||
|
${labels.length > 2
|
||||||
|
? html`<ha-button-menu
|
||||||
|
absolute
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click=${this._handleIconOverflowMenuOpened}
|
||||||
|
@closed=${this._handleIconOverflowMenuClosed}
|
||||||
|
>
|
||||||
|
<ha-label slot="trigger" class="plus" dense>
|
||||||
|
+${labels.length - 2}
|
||||||
|
</ha-label>
|
||||||
|
${repeat(
|
||||||
|
labels.slice(2),
|
||||||
|
(label) => label.label_id,
|
||||||
|
(label) => html`
|
||||||
|
<ha-list-item @click=${this._labelClicked} .item=${label}>
|
||||||
|
${this._renderLabel(label, false)}
|
||||||
|
</ha-list-item>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>`
|
||||||
|
: nothing}
|
||||||
|
</ha-chip-set>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
|
||||||
|
const color = label?.color ? computeCssColor(label.color) : undefined;
|
||||||
|
return html`
|
||||||
|
<ha-label
|
||||||
|
dense
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
.item=${label}
|
||||||
|
@click=${clickAction ? this._labelClicked : undefined}
|
||||||
|
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
>
|
||||||
|
${label?.icon
|
||||||
|
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${label.name}
|
||||||
|
</ha-label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelClicked(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const label = (ev.currentTarget as any).item as LabelRegistryEntry;
|
||||||
|
fireEvent(this, "label-clicked", { label });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleIconOverflowMenuOpened(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// If this component is used inside a data table, the z-index of the row
|
||||||
|
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||||
|
// underneath the next row in the table.
|
||||||
|
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||||
|
if (row) {
|
||||||
|
row.style.zIndex = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleIconOverflowMenuClosed() {
|
||||||
|
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||||
|
if (row) {
|
||||||
|
row.style.zIndex = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
ha-chip-set {
|
||||||
|
position: fixed;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
ha-label {
|
||||||
|
--ha-label-background-color: var(--color, var(--grey-color));
|
||||||
|
--ha-label-background-opacity: 0.5;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.plus {
|
||||||
|
--ha-label-background-color: transparent;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-data-table-labels": HaDataTableLabels;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"label-clicked": { label: LabelRegistryEntry };
|
||||||
|
}
|
||||||
|
}
|
@@ -32,6 +32,8 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@@ -67,13 +69,20 @@ export interface DataTableSortColumnData {
|
|||||||
filterKey?: string;
|
filterKey?: string;
|
||||||
valueColumn?: string;
|
valueColumn?: string;
|
||||||
direction?: SortingDirection;
|
direction?: SortingDirection;
|
||||||
|
groupable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||||
main?: boolean;
|
main?: boolean;
|
||||||
title: TemplateResult | string;
|
title: TemplateResult | string;
|
||||||
label?: TemplateResult | string;
|
label?: TemplateResult | string;
|
||||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
type?:
|
||||||
|
| "numeric"
|
||||||
|
| "icon"
|
||||||
|
| "icon-button"
|
||||||
|
| "overflow"
|
||||||
|
| "overflow-menu"
|
||||||
|
| "flex";
|
||||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||||
width?: string;
|
width?: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
@@ -95,6 +104,8 @@ export interface SortableColumnContainer {
|
|||||||
[key: string]: ClonedDataTableColumnData;
|
[key: string]: ClonedDataTableColumnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||||
|
|
||||||
@customElement("ha-data-table")
|
@customElement("ha-data-table")
|
||||||
export class HaDataTable extends LitElement {
|
export class HaDataTable extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -129,14 +140,16 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@property({ type: String }) public filter = "";
|
@property({ type: String }) public filter = "";
|
||||||
|
|
||||||
|
@property() public groupColumn?: string;
|
||||||
|
|
||||||
|
@property() public sortColumn?: string;
|
||||||
|
|
||||||
|
@property() public sortDirection: SortingDirection = null;
|
||||||
|
|
||||||
@state() private _filterable = false;
|
@state() private _filterable = false;
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
|
|
||||||
@state() private _sortColumn?: string;
|
|
||||||
|
|
||||||
@state() private _sortDirection: SortingDirection = null;
|
|
||||||
|
|
||||||
@state() private _filteredData: DataTableRowData[] = [];
|
@state() private _filteredData: DataTableRowData[] = [];
|
||||||
|
|
||||||
@state() private _headerHeight = 0;
|
@state() private _headerHeight = 0;
|
||||||
@@ -169,6 +182,13 @@ export class HaDataTable extends LitElement {
|
|||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public selectAll(): void {
|
||||||
|
this._checkedRows = this._filteredData
|
||||||
|
.filter((data) => data.selectable !== false)
|
||||||
|
.map((data) => data[this.id]);
|
||||||
|
this._checkedRowsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this._items.length) {
|
if (this._items.length) {
|
||||||
@@ -195,8 +215,14 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
for (const columnId in this.columns) {
|
for (const columnId in this.columns) {
|
||||||
if (this.columns[columnId].direction) {
|
if (this.columns[columnId].direction) {
|
||||||
this._sortDirection = this.columns[columnId].direction!;
|
this.sortDirection = this.columns[columnId].direction!;
|
||||||
this._sortColumn = columnId;
|
this.sortColumn = columnId;
|
||||||
|
|
||||||
|
fireEvent(this, "sorting-changed", {
|
||||||
|
column: columnId,
|
||||||
|
direction: this.sortDirection,
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,11 +252,16 @@ export class HaDataTable extends LitElement {
|
|||||||
properties.has("data") ||
|
properties.has("data") ||
|
||||||
properties.has("columns") ||
|
properties.has("columns") ||
|
||||||
properties.has("_filter") ||
|
properties.has("_filter") ||
|
||||||
properties.has("_sortColumn") ||
|
properties.has("sortColumn") ||
|
||||||
properties.has("_sortDirection")
|
properties.has("sortDirection") ||
|
||||||
|
properties.has("groupColumn")
|
||||||
) {
|
) {
|
||||||
this._sortFilterData();
|
this._sortFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (properties.has("selectable")) {
|
||||||
|
this._items = [...this._items];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@@ -263,6 +294,7 @@ export class HaDataTable extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
||||||
|
<slot name="header-row">
|
||||||
${this.selectable
|
${this.selectable
|
||||||
? html`
|
? html`
|
||||||
<div
|
<div
|
||||||
@@ -285,7 +317,7 @@ export class HaDataTable extends LitElement {
|
|||||||
if (column.hidden) {
|
if (column.hidden) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const sorted = key === this._sortColumn;
|
const sorted = key === this.sortColumn;
|
||||||
const classes = {
|
const classes = {
|
||||||
"mdc-data-table__header-cell--numeric":
|
"mdc-data-table__header-cell--numeric":
|
||||||
column.type === "numeric",
|
column.type === "numeric",
|
||||||
@@ -294,6 +326,8 @@ export class HaDataTable extends LitElement {
|
|||||||
column.type === "icon-button",
|
column.type === "icon-button",
|
||||||
"mdc-data-table__header-cell--overflow-menu":
|
"mdc-data-table__header-cell--overflow-menu":
|
||||||
column.type === "overflow-menu",
|
column.type === "overflow-menu",
|
||||||
|
"mdc-data-table__header-cell--overflow":
|
||||||
|
column.type === "overflow",
|
||||||
sortable: Boolean(column.sortable),
|
sortable: Boolean(column.sortable),
|
||||||
"not-sorted": Boolean(column.sortable && !sorted),
|
"not-sorted": Boolean(column.sortable && !sorted),
|
||||||
grows: Boolean(column.grows),
|
grows: Boolean(column.grows),
|
||||||
@@ -311,7 +345,7 @@ export class HaDataTable extends LitElement {
|
|||||||
role="columnheader"
|
role="columnheader"
|
||||||
aria-sort=${ifDefined(
|
aria-sort=${ifDefined(
|
||||||
sorted
|
sorted
|
||||||
? this._sortDirection === "desc"
|
? this.sortDirection === "desc"
|
||||||
? "descending"
|
? "descending"
|
||||||
: "ascending"
|
: "ascending"
|
||||||
: undefined
|
: undefined
|
||||||
@@ -322,7 +356,7 @@ export class HaDataTable extends LitElement {
|
|||||||
${column.sortable
|
${column.sortable
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${sorted && this._sortDirection === "desc"
|
.path=${sorted && this.sortDirection === "desc"
|
||||||
? mdiArrowDown
|
? mdiArrowDown
|
||||||
: mdiArrowUp}
|
: mdiArrowUp}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
@@ -332,6 +366,7 @@ export class HaDataTable extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
${!this._filteredData.length
|
${!this._filteredData.length
|
||||||
? html`
|
? html`
|
||||||
@@ -359,7 +394,7 @@ export class HaDataTable extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _keyFunction = (row: DataTableRowData) => row[this.id] || row;
|
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
|
||||||
|
|
||||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||||
// not sure how this happens...
|
// not sure how this happens...
|
||||||
@@ -408,7 +443,7 @@ export class HaDataTable extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${Object.entries(this.columns).map(([key, column]) => {
|
${Object.entries(this.columns).map(([key, column]) => {
|
||||||
if (column.hidden) {
|
if (column.hidden) {
|
||||||
return "";
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@@ -421,6 +456,7 @@ export class HaDataTable extends LitElement {
|
|||||||
column.type === "icon-button",
|
column.type === "icon-button",
|
||||||
"mdc-data-table__cell--overflow-menu":
|
"mdc-data-table__cell--overflow-menu":
|
||||||
column.type === "overflow-menu",
|
column.type === "overflow-menu",
|
||||||
|
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||||
grows: Boolean(column.grows),
|
grows: Boolean(column.grows),
|
||||||
forceLTR: Boolean(column.forceLTR),
|
forceLTR: Boolean(column.forceLTR),
|
||||||
})}"
|
})}"
|
||||||
@@ -453,12 +489,12 @@ export class HaDataTable extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prom = this._sortColumn
|
const prom = this.sortColumn
|
||||||
? sortData(
|
? sortData(
|
||||||
filteredData,
|
filteredData,
|
||||||
this._sortColumns[this._sortColumn],
|
this._sortColumns[this.sortColumn],
|
||||||
this._sortDirection,
|
this.sortDirection,
|
||||||
this._sortColumn,
|
this.sortColumn,
|
||||||
this.hass.locale.language
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
: filteredData;
|
: filteredData;
|
||||||
@@ -477,17 +513,62 @@ export class HaDataTable extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.appendRow || this.hasFab) {
|
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||||
const items = [...data];
|
const items = [...data];
|
||||||
|
|
||||||
if (this.appendRow) {
|
if (this.appendRow) {
|
||||||
items.push({ append: true, content: this.appendRow });
|
items.push({ append: true, content: this.appendRow });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasFab) {
|
if (this.groupColumn) {
|
||||||
items.push({ empty: true });
|
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||||
|
if (grouped.undefined) {
|
||||||
|
// make sure ungrouped items are at the bottom
|
||||||
|
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||||
|
delete grouped.undefined;
|
||||||
}
|
}
|
||||||
|
const sorted: {
|
||||||
|
[key: string]: DataTableRowData[];
|
||||||
|
} = Object.keys(grouped)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||||
|
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = grouped[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
const groupedItems: DataTableRowData[] = [];
|
||||||
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
|
if (
|
||||||
|
groupName !== UNDEFINED_GROUP_KEY ||
|
||||||
|
Object.keys(sorted).length > 1
|
||||||
|
) {
|
||||||
|
groupedItems.push({
|
||||||
|
append: true,
|
||||||
|
content: html`<div
|
||||||
|
class="mdc-data-table__cell group-header"
|
||||||
|
role="cell"
|
||||||
|
>
|
||||||
|
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedItems.push(...rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._items = groupedItems;
|
||||||
|
} else {
|
||||||
this._items = items;
|
this._items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasFab) {
|
||||||
|
this._items = [...this._items, { empty: true }];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._items = data;
|
this._items = data;
|
||||||
}
|
}
|
||||||
@@ -507,29 +588,26 @@ export class HaDataTable extends LitElement {
|
|||||||
if (!this.columns[columnId].sortable) {
|
if (!this.columns[columnId].sortable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
if (!this.sortDirection || this.sortColumn !== columnId) {
|
||||||
this._sortDirection = "asc";
|
this.sortDirection = "asc";
|
||||||
} else if (this._sortDirection === "asc") {
|
} else if (this.sortDirection === "asc") {
|
||||||
this._sortDirection = "desc";
|
this.sortDirection = "desc";
|
||||||
} else {
|
} else {
|
||||||
this._sortDirection = null;
|
this.sortDirection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
this.sortColumn = this.sortDirection === null ? undefined : columnId;
|
||||||
|
|
||||||
fireEvent(this, "sorting-changed", {
|
fireEvent(this, "sorting-changed", {
|
||||||
column: columnId,
|
column: columnId,
|
||||||
direction: this._sortDirection,
|
direction: this.sortDirection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||||
const checkbox = ev.target as HaCheckbox;
|
const checkbox = ev.target as HaCheckbox;
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
this._checkedRows = this._filteredData
|
this.selectAll();
|
||||||
.filter((data) => data.selectable !== false)
|
|
||||||
.map((data) => data[this.id]);
|
|
||||||
this._checkedRowsChanged();
|
|
||||||
} else {
|
} else {
|
||||||
this._checkedRows = [];
|
this._checkedRows = [];
|
||||||
this._checkedRowsChanged();
|
this._checkedRowsChanged();
|
||||||
@@ -552,8 +630,19 @@ export class HaDataTable extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _handleRowClick = (ev: Event) => {
|
private _handleRowClick = (ev: Event) => {
|
||||||
const target = ev.target as HTMLElement;
|
if (
|
||||||
if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) {
|
ev
|
||||||
|
.composedPath()
|
||||||
|
.find((el) =>
|
||||||
|
[
|
||||||
|
"ha-checkbox",
|
||||||
|
"mwc-button",
|
||||||
|
"ha-button",
|
||||||
|
"ha-icon-button",
|
||||||
|
"ha-assist-chip",
|
||||||
|
].includes((el as HTMLElement).localName)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rowId = (ev.currentTarget as any).rowId;
|
const rowId = (ev.currentTarget as any).rowId;
|
||||||
@@ -629,7 +718,7 @@ export class HaDataTable extends LitElement {
|
|||||||
.mdc-data-table__row {
|
.mdc-data-table__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 52px;
|
height: var(--data-table-row-height, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||||
@@ -655,7 +744,6 @@ export class HaDataTable extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||||
@@ -809,7 +897,9 @@ export class HaDataTable extends LitElement {
|
|||||||
padding-inline-start: initial;
|
padding-inline-start: initial;
|
||||||
}
|
}
|
||||||
.mdc-data-table__cell--overflow-menu,
|
.mdc-data-table__cell--overflow-menu,
|
||||||
.mdc-data-table__header-cell--overflow-menu {
|
.mdc-data-table__cell--overflow,
|
||||||
|
.mdc-data-table__header-cell--overflow-menu,
|
||||||
|
.mdc-data-table__header-cell--overflow {
|
||||||
overflow: initial;
|
overflow: initial;
|
||||||
}
|
}
|
||||||
.mdc-data-table__cell--icon-button a {
|
.mdc-data-table__cell--icon-button a {
|
||||||
@@ -839,6 +929,12 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
/* custom from here */
|
/* custom from here */
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
padding-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import { mdiSofa } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
|
import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon-next";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-textfield";
|
import "./ha-textfield";
|
||||||
import "./ha-icon-next";
|
|
||||||
|
|
||||||
export type AreaFilterValue = {
|
export type AreaFilterValue = {
|
||||||
hidden?: string[];
|
hidden?: string[];
|
||||||
@@ -51,7 +51,7 @@ export class HaAreaPicker extends LitElement {
|
|||||||
@keydown=${this._edit}
|
@keydown=${this._edit}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiSofa}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>
|
||||||
<span>${this.label}</span>
|
<span>${this.label}</span>
|
||||||
<span slot="secondary">${description}</span>
|
<span slot="secondary">${description}</span>
|
||||||
<ha-icon-next
|
<ha-icon-next
|
||||||
|
529
src/components/ha-area-floor-picker.ts
Normal file
529
src/components/ha-area-floor-picker.ts
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import {
|
||||||
|
ScorableTextItem,
|
||||||
|
fuzzyFilterSort,
|
||||||
|
} from "../common/string/filter/sequence-matching";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import { AreaRegistryEntry } from "../data/area_registry";
|
||||||
|
import {
|
||||||
|
DeviceEntityDisplayLookup,
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
} from "../data/device_registry";
|
||||||
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
|
import {
|
||||||
|
FloorRegistryEntry,
|
||||||
|
getFloorAreaLookup,
|
||||||
|
subscribeFloorRegistry,
|
||||||
|
} from "../data/floor_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
|
import "./ha-floor-icon";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-list-item";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
|
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
||||||
|
|
||||||
|
interface FloorAreaEntry {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
strings: string[];
|
||||||
|
type: "floor" | "area";
|
||||||
|
level: number | null;
|
||||||
|
hasFloor?: boolean;
|
||||||
|
lastArea?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-area-floor-picker")
|
||||||
|
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only areas with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no areas with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only areas with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of areas to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-areas
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-areas" })
|
||||||
|
public excludeAreas?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of floors to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-floors
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-floors" })
|
||||||
|
public excludeFloors?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
|
this._floors = floors;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
|
||||||
|
const rtl = computeRTL(this.hass);
|
||||||
|
return html`
|
||||||
|
<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
style=${item.type === "area" && item.hasFloor
|
||||||
|
? rtl
|
||||||
|
? "--mdc-list-side-padding-right: 48px;"
|
||||||
|
: "--mdc-list-side-padding-left: 48px;"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${item.type === "area" && item.hasFloor
|
||||||
|
? html`<ha-tree-indicator
|
||||||
|
style=${styleMap({
|
||||||
|
width: "48px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "0px",
|
||||||
|
left: rtl ? undefined : "8px",
|
||||||
|
right: rtl ? "8px" : undefined,
|
||||||
|
transform: rtl ? "scaleX(-1)" : "",
|
||||||
|
})}
|
||||||
|
.end=${item.lastArea}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-tree-indicator>`
|
||||||
|
: nothing}
|
||||||
|
${item.type === "floor"
|
||||||
|
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
|
||||||
|
: item.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getAreas = memoizeOne(
|
||||||
|
(
|
||||||
|
floors: FloorRegistryEntry[],
|
||||||
|
areas: AreaRegistryEntry[],
|
||||||
|
devices: DeviceRegistryEntry[],
|
||||||
|
entities: EntityRegistryDisplayEntry[],
|
||||||
|
includeDomains: this["includeDomains"],
|
||||||
|
excludeDomains: this["excludeDomains"],
|
||||||
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
|
deviceFilter: this["deviceFilter"],
|
||||||
|
entityFilter: this["entityFilter"],
|
||||||
|
excludeAreas: this["excludeAreas"],
|
||||||
|
excludeFloors: this["excludeFloors"]
|
||||||
|
): FloorAreaEntry[] => {
|
||||||
|
if (!areas.length && !floors.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "no_areas",
|
||||||
|
type: "area",
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||||
|
icon: null,
|
||||||
|
strings: [],
|
||||||
|
level: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
|
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
includeDomains ||
|
||||||
|
excludeDomains ||
|
||||||
|
includeDeviceClasses ||
|
||||||
|
deviceFilter ||
|
||||||
|
entityFilter
|
||||||
|
) {
|
||||||
|
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||||
|
inputDevices = devices;
|
||||||
|
inputEntities = entities.filter((entity) => entity.area_id);
|
||||||
|
|
||||||
|
if (includeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entities.every(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) =>
|
||||||
|
deviceFilter!(device)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter!(stateObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputAreas = areas;
|
||||||
|
|
||||||
|
let areaIds: string[] | undefined;
|
||||||
|
|
||||||
|
if (inputDevices) {
|
||||||
|
areaIds = inputDevices
|
||||||
|
.filter((device) => device.area_id)
|
||||||
|
.map((device) => device.area_id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputEntities) {
|
||||||
|
areaIds = (areaIds ?? []).concat(
|
||||||
|
inputEntities
|
||||||
|
.filter((entity) => entity.area_id)
|
||||||
|
.map((entity) => entity.area_id!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds) {
|
||||||
|
outputAreas = outputAreas.filter((area) =>
|
||||||
|
areaIds!.includes(area.area_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeAreas) {
|
||||||
|
outputAreas = outputAreas.filter(
|
||||||
|
(area) => !excludeAreas!.includes(area.area_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeFloors) {
|
||||||
|
outputAreas = outputAreas.filter(
|
||||||
|
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputAreas.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "no_areas",
|
||||||
|
type: "area",
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
strings: [],
|
||||||
|
level: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||||
|
const unassisgnedAreas = Object.values(outputAreas).filter(
|
||||||
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const floorAreaEntries: Array<
|
||||||
|
[FloorRegistryEntry | undefined, AreaRegistryEntry[]]
|
||||||
|
> = Object.entries(floorAreaLookup)
|
||||||
|
.map(([floorId, floorAreas]) => {
|
||||||
|
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||||
|
return [floor, floorAreas] as const;
|
||||||
|
})
|
||||||
|
.sort(([floorA], [floorB]) => {
|
||||||
|
if (floorA.level !== floorB.level) {
|
||||||
|
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||||
|
}
|
||||||
|
return stringCompare(floorA.name, floorB.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const output: FloorAreaEntry[] = [];
|
||||||
|
|
||||||
|
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||||
|
if (floor) {
|
||||||
|
output.push({
|
||||||
|
id: floor.floor_id,
|
||||||
|
type: "floor",
|
||||||
|
name: floor.name,
|
||||||
|
icon: floor.icon,
|
||||||
|
strings: [floor.floor_id, ...floor.aliases, floor.name],
|
||||||
|
level: floor.level,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
output.push(
|
||||||
|
...floorAreas.map((area, index, array) => ({
|
||||||
|
id: area.area_id,
|
||||||
|
type: "area" as const,
|
||||||
|
name: area.name,
|
||||||
|
icon: area.icon,
|
||||||
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
hasFloor: true,
|
||||||
|
level: null,
|
||||||
|
lastArea: index === array.length - 1,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!output.length && !unassisgnedAreas.length) {
|
||||||
|
output.push({
|
||||||
|
id: "no_areas",
|
||||||
|
type: "area",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.area-picker.unassigned_areas"
|
||||||
|
),
|
||||||
|
icon: null,
|
||||||
|
strings: [],
|
||||||
|
level: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(
|
||||||
|
...unassisgnedAreas.map((area) => ({
|
||||||
|
id: area.area_id,
|
||||||
|
type: "area" as const,
|
||||||
|
name: area.name,
|
||||||
|
icon: area.icon,
|
||||||
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
level: null,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass && this._floors) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const areas = this._getAreas(
|
||||||
|
this._floors!,
|
||||||
|
Object.values(this.hass.areas),
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.excludeAreas,
|
||||||
|
this.excludeFloors
|
||||||
|
);
|
||||||
|
this.comboBox.items = areas;
|
||||||
|
this.comboBox.filteredItems = areas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
item-value-path="id"
|
||||||
|
item-id-path="id"
|
||||||
|
item-label-path="name"
|
||||||
|
.value=${this._value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.area-picker.area")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder
|
||||||
|
? this.hass.areas[this.placeholder]?.name
|
||||||
|
: undefined}
|
||||||
|
.renderer=${this._rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._areaChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = fuzzyFilterSort<ScorableAreaFloorEntry>(
|
||||||
|
filterString,
|
||||||
|
target.items || []
|
||||||
|
);
|
||||||
|
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _areaChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === "no_areas") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = this.comboBox.selectedItem;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
id: selected.id,
|
||||||
|
type: selected.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-area-floor-picker": HaAreaFloorPicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,14 +1,15 @@
|
|||||||
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { HassEntity } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
|
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import {
|
import {
|
||||||
fuzzyFilterSort,
|
|
||||||
ScorableTextItem,
|
ScorableTextItem,
|
||||||
|
fuzzyFilterSort,
|
||||||
} from "../common/string/filter/sequence-matching";
|
} from "../common/string/filter/sequence-matching";
|
||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
@@ -20,10 +21,8 @@ import {
|
|||||||
getDeviceEntityDisplayLookup,
|
getDeviceEntityDisplayLookup,
|
||||||
} from "../data/device_registry";
|
} from "../data/device_registry";
|
||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import {
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
showAlertDialog,
|
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||||
showPromptDialog,
|
|
||||||
} from "../dialogs/generic/show-dialog-box";
|
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
@@ -37,14 +36,18 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
|||||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
|
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
|
||||||
html`<ha-list-item
|
html`<ha-list-item
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
class=${classMap({ "add-new": item.area_id === "add_new" })}
|
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
|
||||||
>
|
>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
: nothing}
|
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
|
||||||
${item.name}
|
${item.name}
|
||||||
</ha-list-item>`;
|
</ha-list-item>`;
|
||||||
|
|
||||||
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
const NO_ITEMS_ID = "___NO_ITEMS___";
|
||||||
|
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||||
|
|
||||||
@customElement("ha-area-picker")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends LitElement {
|
export class HaAreaPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -133,18 +136,6 @@ export class HaAreaPicker extends LitElement {
|
|||||||
noAdd: this["noAdd"],
|
noAdd: this["noAdd"],
|
||||||
excludeAreas: this["excludeAreas"]
|
excludeAreas: this["excludeAreas"]
|
||||||
): AreaRegistryEntry[] => {
|
): AreaRegistryEntry[] => {
|
||||||
if (!areas.length) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
area_id: "no_areas",
|
|
||||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
|
||||||
picture: null,
|
|
||||||
icon: null,
|
|
||||||
aliases: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||||
@@ -281,11 +272,13 @@ export class HaAreaPicker extends LitElement {
|
|||||||
if (!outputAreas.length) {
|
if (!outputAreas.length) {
|
||||||
outputAreas = [
|
outputAreas = [
|
||||||
{
|
{
|
||||||
area_id: "no_areas",
|
area_id: NO_ITEMS_ID,
|
||||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
floor_id: null,
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||||
picture: null,
|
picture: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -295,11 +288,13 @@ export class HaAreaPicker extends LitElement {
|
|||||||
: [
|
: [
|
||||||
...outputAreas,
|
...outputAreas,
|
||||||
{
|
{
|
||||||
area_id: "add_new",
|
area_id: ADD_NEW_ID,
|
||||||
|
floor_id: null,
|
||||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||||
picture: null,
|
picture: null,
|
||||||
icon: "mdi:plus",
|
icon: "mdi:plus",
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
labels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -367,20 +362,40 @@ export class HaAreaPicker extends LitElement {
|
|||||||
|
|
||||||
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||||
filterString,
|
filterString,
|
||||||
target.items || []
|
target.items?.filter(
|
||||||
|
(item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||||
|
) || []
|
||||||
);
|
);
|
||||||
if (!this.noAdd && filteredItems?.length === 0) {
|
if (filteredItems.length === 0) {
|
||||||
|
if (!this.noAdd) {
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
area_id: NO_ITEMS_ID,
|
||||||
|
floor_id: null,
|
||||||
|
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
picture: null,
|
||||||
|
labels: [],
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
] as AreaRegistryEntry[];
|
||||||
|
} else {
|
||||||
this._suggestion = filterString;
|
this._suggestion = filterString;
|
||||||
this.comboBox.filteredItems = [
|
this.comboBox.filteredItems = [
|
||||||
{
|
{
|
||||||
area_id: "add_new_suggestion",
|
area_id: ADD_NEW_SUGGESTION_ID,
|
||||||
|
floor_id: null,
|
||||||
name: this.hass.localize(
|
name: this.hass.localize(
|
||||||
"ui.components.area-picker.add_new_sugestion",
|
"ui.components.area-picker.add_new_sugestion",
|
||||||
{ name: this._suggestion }
|
{ name: this._suggestion }
|
||||||
),
|
),
|
||||||
|
icon: "mdi:plus",
|
||||||
picture: null,
|
picture: null,
|
||||||
|
labels: [],
|
||||||
|
aliases: [],
|
||||||
},
|
},
|
||||||
];
|
] as AreaRegistryEntry[];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.comboBox.filteredItems = filteredItems;
|
this.comboBox.filteredItems = filteredItems;
|
||||||
}
|
}
|
||||||
@@ -398,11 +413,13 @@ export class HaAreaPicker extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
let newValue = ev.detail.value;
|
let newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue === "no_areas") {
|
if (newValue === NO_ITEMS_ID) {
|
||||||
newValue = "";
|
newValue = "";
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
|
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this._value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
@@ -410,25 +427,14 @@ export class HaAreaPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
showPromptDialog(this, {
|
|
||||||
title: this.hass.localize("ui.components.area-picker.add_dialog.title"),
|
this.hass.loadFragmentTranslation("config");
|
||||||
text: this.hass.localize("ui.components.area-picker.add_dialog.text"),
|
|
||||||
confirmText: this.hass.localize(
|
showAreaRegistryDetailDialog(this, {
|
||||||
"ui.components.area-picker.add_dialog.add"
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
),
|
createEntry: async (values) => {
|
||||||
inputLabel: this.hass.localize(
|
|
||||||
"ui.components.area-picker.add_dialog.name"
|
|
||||||
),
|
|
||||||
defaultValue:
|
|
||||||
newValue === "add_new_suggestion" ? this._suggestion : undefined,
|
|
||||||
confirm: async (name) => {
|
|
||||||
if (!name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const area = await createAreaRegistryEntry(this.hass, {
|
const area = await createAreaRegistryEntry(this.hass, values);
|
||||||
name,
|
|
||||||
});
|
|
||||||
const areas = [...Object.values(this.hass.areas), area];
|
const areas = [...Object.values(this.hass.areas), area];
|
||||||
this.comboBox.filteredItems = this._getAreas(
|
this.comboBox.filteredItems = this._getAreas(
|
||||||
areas,
|
areas,
|
||||||
@@ -448,18 +454,16 @@ export class HaAreaPicker extends LitElement {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.components.area-picker.add_dialog.failed_create_area"
|
"ui.components.area-picker.failed_create_area"
|
||||||
),
|
),
|
||||||
text: err.message,
|
text: err.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel: () => {
|
});
|
||||||
this._setValue(undefined);
|
|
||||||
this._suggestion = undefined;
|
this._suggestion = undefined;
|
||||||
this.comboBox.setInputValue("");
|
this.comboBox.setInputValue("");
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value?: string) {
|
private _setValue(value?: string) {
|
||||||
|
89
src/components/ha-button-menu-new.ts
Normal file
89
src/components/ha-button-menu-new.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Button } from "@material/mwc-button";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||||
|
import type { HaIconButton } from "./ha-icon-button";
|
||||||
|
import "./ha-menu";
|
||||||
|
import type { HaMenu } from "./ha-menu";
|
||||||
|
|
||||||
|
@customElement("ha-button-menu-new")
|
||||||
|
export class HaButtonMenuNew extends LitElement {
|
||||||
|
protected readonly [FOCUS_TARGET];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property() public positioning?: "fixed" | "absolute" | "popover";
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
|
||||||
|
false;
|
||||||
|
|
||||||
|
@query("ha-menu", true) private _menu!: HaMenu;
|
||||||
|
|
||||||
|
public get items() {
|
||||||
|
return this._menu.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override focus() {
|
||||||
|
if (this._menu.open) {
|
||||||
|
this._menu.focus();
|
||||||
|
} else {
|
||||||
|
this._triggerButton?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div @click=${this._handleClick}>
|
||||||
|
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||||
|
</div>
|
||||||
|
<ha-menu
|
||||||
|
.positioning=${this.positioning}
|
||||||
|
.hasOverflow=${this.hasOverflow}
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</ha-menu>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick(): void {
|
||||||
|
if (this.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._menu.anchorElement = this;
|
||||||
|
if (this._menu.open) {
|
||||||
|
this._menu.close();
|
||||||
|
} else {
|
||||||
|
this._menu.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _triggerButton() {
|
||||||
|
return this.querySelector(
|
||||||
|
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"], ha-assist-chip[slot="trigger"]'
|
||||||
|
) as HaIconButton | Button | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setTriggerAria() {
|
||||||
|
if (this._triggerButton) {
|
||||||
|
this._triggerButton.ariaHasPopup = "menu";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
::slotted([disabled]) {
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-button-menu-new": HaButtonMenuNew;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,221 +0,0 @@
|
|||||||
import type { Corner } from "@material/mwc-menu";
|
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
|
||||||
import { mdiFilterVariant } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { computeDeviceName } from "../data/device_registry";
|
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./device/ha-device-picker";
|
|
||||||
import "./entity/ha-entity-picker";
|
|
||||||
import "./ha-area-picker";
|
|
||||||
import "./ha-icon-button";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// for fire event
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"related-changed": {
|
|
||||||
value?: FilterValue;
|
|
||||||
items?: RelatedResult;
|
|
||||||
filter?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterValue {
|
|
||||||
area?: string;
|
|
||||||
device?: string;
|
|
||||||
entity?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-button-related-filter-menu")
|
|
||||||
export class HaRelatedFilterButtonMenu extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public corner: Corner = "BOTTOM_START";
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: FilterValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show no entities of these domains.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr exclude-domains
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "exclude-domains" })
|
|
||||||
public excludeDomains?: string[];
|
|
||||||
|
|
||||||
@state() private _open = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<ha-icon-button
|
|
||||||
@click=${this._handleClick}
|
|
||||||
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
|
|
||||||
.path=${mdiFilterVariant}
|
|
||||||
></ha-icon-button>
|
|
||||||
<mwc-menu-surface
|
|
||||||
.open=${this._open}
|
|
||||||
.anchor=${this}
|
|
||||||
.fullwidth=${this.narrow}
|
|
||||||
.corner=${this.corner}
|
|
||||||
@closed=${this._onClosed}
|
|
||||||
@input=${stopPropagation}
|
|
||||||
>
|
|
||||||
<ha-area-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_area"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.area}
|
|
||||||
no-add
|
|
||||||
@value-changed=${this._areaPicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-area-picker>
|
|
||||||
<ha-device-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_device"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.device}
|
|
||||||
@value-changed=${this._devicePicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-device-picker>
|
|
||||||
<ha-entity-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_entity"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.entity}
|
|
||||||
.excludeDomains=${this.excludeDomains}
|
|
||||||
@value-changed=${this._entityPicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-entity-picker>
|
|
||||||
</mwc-menu-surface>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleClick(): void {
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onClosed(ev): void {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _preventDefault(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _entityPicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const entityId = ev.detail.value;
|
|
||||||
if (!entityId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_entity",
|
|
||||||
{
|
|
||||||
entity_name: computeStateName(
|
|
||||||
(ev.currentTarget as any).comboBox.selectedItem
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "entity", entityId);
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { entity: entityId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _devicePicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const deviceId = ev.detail.value;
|
|
||||||
if (!deviceId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_device",
|
|
||||||
{
|
|
||||||
device_name: computeDeviceName(
|
|
||||||
(ev.currentTarget as any).comboBox.selectedItem,
|
|
||||||
this.hass
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "device", deviceId);
|
|
||||||
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { device: deviceId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _areaPicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const areaId = ev.detail.value;
|
|
||||||
if (!areaId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_area",
|
|
||||||
{ area_name: (ev.currentTarget as any).comboBox.selectedItem.name }
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "area", areaId);
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { area: areaId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
--mdc-menu-min-width: 250px;
|
|
||||||
}
|
|
||||||
ha-area-picker,
|
|
||||||
ha-device-picker,
|
|
||||||
ha-entity-picker {
|
|
||||||
display: block;
|
|
||||||
width: 300px;
|
|
||||||
padding: 4px 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
ha-area-picker {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
ha-entity-picker {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
:host([narrow]) ha-area-picker,
|
|
||||||
:host([narrow]) ha-device-picker,
|
|
||||||
:host([narrow]) ha-entity-picker {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,17 +2,16 @@ import "@material/mwc-list/mwc-list-item";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import {
|
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||||
computeCssColor,
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
THEME_COLORS,
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
} from "../../../common/color/compute-color";
|
import "./ha-select";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import "./ha-list-item";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { HomeAssistant } from "../types";
|
||||||
import "../../../components/ha-select";
|
import { LocalizeKeys } from "../common/translations/localize";
|
||||||
import { HomeAssistant } from "../../../types";
|
|
||||||
|
|
||||||
@customElement("hui-color-picker")
|
@customElement("ha-color-picker")
|
||||||
export class HuiColorPicker extends LitElement {
|
export class HaColorPicker extends LitElement {
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
@@ -21,6 +20,8 @@ export class HuiColorPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public defaultColor = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
_valueSelected(ev) {
|
_valueSelected(ev) {
|
||||||
@@ -52,19 +53,19 @@ export class HuiColorPicker extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<mwc-list-item value="default">
|
${this.defaultColor
|
||||||
${this.hass.localize(
|
? html` <ha-list-item value="default">
|
||||||
`ui.panel.lovelace.editor.color-picker.default_color`
|
${this.hass.localize(`ui.components.color-picker.default_color`)}
|
||||||
)}
|
</ha-list-item>`
|
||||||
</mwc-list-item>
|
: nothing}
|
||||||
${Array.from(THEME_COLORS).map(
|
${Array.from(THEME_COLORS).map(
|
||||||
(color) => html`
|
(color) => html`
|
||||||
<mwc-list-item .value=${color} graphic="icon">
|
<ha-list-item .value=${color} graphic="icon">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.lovelace.editor.color-picker.colors.${color}`
|
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||||
) || color}
|
) || color}
|
||||||
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
<span slot="graphic">${this.renderColorCircle(color)}</span>
|
||||||
</mwc-list-item>
|
</ha-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-select>
|
</ha-select>
|
||||||
@@ -100,6 +101,6 @@ export class HuiColorPicker extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hui-color-picker": HuiColorPicker;
|
"ha-color-picker": HaColorPicker;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -84,6 +84,7 @@ export class HaControlButton extends LitElement {
|
|||||||
--control-button-background-color: var(--disabled-color);
|
--control-button-background-color: var(--disabled-color);
|
||||||
--control-button-background-opacity: 0.2;
|
--control-button-background-opacity: 0.2;
|
||||||
--control-button-border-radius: 10px;
|
--control-button-border-radius: 10px;
|
||||||
|
--control-button-padding: 8px;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -95,16 +96,20 @@ export class HaControlButton extends LitElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: var(--control-button-border-radius);
|
border-radius: var(--control-button-border-radius);
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: var(--control-button-padding);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: 0;
|
line-height: inherit;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-weight: 500;
|
||||||
outline: none;
|
outline: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -126,6 +131,8 @@ export class HaControlButton extends LitElement {
|
|||||||
background-color 180ms ease-in-out,
|
background-color 180ms ease-in-out,
|
||||||
opacity 180ms ease-in-out;
|
opacity 180ms ease-in-out;
|
||||||
opacity: var(--control-button-background-opacity);
|
opacity: var(--control-button-background-opacity);
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
.button {
|
.button {
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
@@ -133,6 +140,7 @@ export class HaControlButton extends LitElement {
|
|||||||
}
|
}
|
||||||
.button ::slotted(*) {
|
.button ::slotted(*) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
.button:disabled {
|
.button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@@ -529,7 +529,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
border-radius: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
.slider .slider-track-bar:after {
|
.slider .slider-track-bar:after {
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -546,7 +546,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
border-radius: 8px 0 0 8px;
|
||||||
}
|
}
|
||||||
.slider .slider-track-bar.end::after {
|
.slider .slider-track-bar.end::after {
|
||||||
right: initial;
|
right: initial;
|
||||||
@@ -561,7 +561,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
calc((1 - var(--value, 0)) * var(--slider-size)),
|
calc((1 - var(--value, 0)) * var(--slider-size)),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
}
|
}
|
||||||
:host([vertical]) .slider .slider-track-bar:after {
|
:host([vertical]) .slider .slider-track-bar:after {
|
||||||
top: var(--handle-margin);
|
top: var(--handle-margin);
|
||||||
@@ -579,7 +579,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
calc((0 - var(--value, 0)) * var(--slider-size)),
|
calc((0 - var(--value, 0)) * var(--slider-size)),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
border-radius: 0 0 8px 8px;
|
||||||
}
|
}
|
||||||
:host([vertical]) .slider .slider-track-bar.end::after {
|
:host([vertical]) .slider .slider-track-bar.end::after {
|
||||||
top: initial;
|
top: initial;
|
||||||
|
@@ -139,12 +139,12 @@ export class HaDialog extends DialogBase {
|
|||||||
}
|
}
|
||||||
.header_button {
|
.header_button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -8px;
|
right: -12px;
|
||||||
top: -8px;
|
top: -12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
inset-inline-end: -8px;
|
inset-inline-end: -12px;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
|
@@ -83,13 +83,11 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues) {
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (changedProps.has("expanded") && this.expanded) {
|
if (changedProps.has("expanded")) {
|
||||||
this._showContent = this.expanded;
|
this._showContent = this.expanded;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Verify we're still expanded
|
// Verify we're still expanded
|
||||||
if (this.expanded) {
|
this._container.style.overflow = this.expanded ? "initial" : "hidden";
|
||||||
this._container.style.overflow = "initial";
|
|
||||||
}
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
193
src/components/ha-filter-blueprints.ts
Normal file
193
src/components/ha-filter-blueprints.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
@customElement("ha-filter-blueprints")
|
||||||
|
export class HaFilterBlueprints extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: "automation" | "script";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _blueprints?: Blueprints;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._blueprints && this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._blueprintsSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${Object.entries(this._blueprints).map(([id, blueprint]) =>
|
||||||
|
"error" in blueprint
|
||||||
|
? nothing
|
||||||
|
: html`<ha-check-list-item
|
||||||
|
.value=${id}
|
||||||
|
.selected=${(this.value || []).includes(id)}
|
||||||
|
>
|
||||||
|
${blueprint.metadata.name || id}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async firstUpdated() {
|
||||||
|
if (!this.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._blueprints = await fetchBlueprints(this.hass, this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.narrow || !this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _blueprintsSelected(
|
||||||
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
|
) {
|
||||||
|
const blueprints = this._blueprints!;
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const blueprintId = Object.keys(blueprints)[index];
|
||||||
|
value.push(blueprintId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(
|
||||||
|
findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-blueprints": HaFilterBlueprints;
|
||||||
|
}
|
||||||
|
}
|
334
src/components/ha-filter-categories.ts
Normal file
334
src/components/ha-filter-categories.ts
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
||||||
|
import {
|
||||||
|
mdiDelete,
|
||||||
|
mdiDotsVertical,
|
||||||
|
mdiFilterVariantRemove,
|
||||||
|
mdiPencil,
|
||||||
|
mdiPlus,
|
||||||
|
mdiTag,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
|
deleteCategoryRegistryEntry,
|
||||||
|
subscribeCategoryRegistry,
|
||||||
|
updateCategoryRegistryEntry,
|
||||||
|
} from "../data/category_registry";
|
||||||
|
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-list-item";
|
||||||
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
|
|
||||||
|
@customElement("ha-filter-categories")
|
||||||
|
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public scope?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _categories: CategoryRegistryEntry[] = [];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["scope"];
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeCategoryRegistry(
|
||||||
|
this.hass.connection,
|
||||||
|
this.scope!,
|
||||||
|
(categories) => {
|
||||||
|
this._categories = categories;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.category.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._categorySelected}
|
||||||
|
class="ha-scrollbar"
|
||||||
|
activatable
|
||||||
|
>
|
||||||
|
${this._categories.length > 0
|
||||||
|
? html`<ha-list-item
|
||||||
|
.selected=${!this.value?.length}
|
||||||
|
.activated=${!this.value?.length}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.filter.show_all"
|
||||||
|
)}</ha-list-item
|
||||||
|
>`
|
||||||
|
: nothing}
|
||||||
|
${this._categories.map(
|
||||||
|
(category) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
.value=${category.category_id}
|
||||||
|
.selected=${this.value?.includes(category.category_id)}
|
||||||
|
.activated=${this.value?.includes(category.category_id)}
|
||||||
|
graphic="icon"
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
${category.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${category.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
.path=${mdiTag}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${category.name}
|
||||||
|
<ha-button-menu
|
||||||
|
@click=${stopPropagation}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
slot="meta"
|
||||||
|
fixed
|
||||||
|
.categoryId=${category.category_id}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
slot="trigger"
|
||||||
|
></ha-icon-button>
|
||||||
|
<mwc-list-item graphic="icon"
|
||||||
|
><ha-svg-icon
|
||||||
|
.path=${mdiPencil}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-svg-icon
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.edit"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
<mwc-list-item graphic="icon" class="warning"
|
||||||
|
><ha-svg-icon
|
||||||
|
class="warning"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-svg-icon
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.delete"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
</ha-button-menu>
|
||||||
|
</ha-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
${this.expanded
|
||||||
|
? html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
@click=${this._addCategory}
|
||||||
|
class="add"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||||
|
</ha-list-item>`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - (49 + 48)}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
const categoryId = (ev.currentTarget as any).categoryId;
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._editCategory(categoryId);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._deleteCategory(categoryId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editCategory(id: string) {
|
||||||
|
showCategoryRegistryDetailDialog(this, {
|
||||||
|
scope: this.scope!,
|
||||||
|
entry: this._categories.find((cat) => cat.category_id === id),
|
||||||
|
updateEntry: (updates) =>
|
||||||
|
updateCategoryRegistryEntry(this.hass, this.scope!, id, updates),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteCategory(id: string) {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.confirm_delete"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.confirm_delete_text"
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deleteCategoryRegistryEntry(this.hass, this.scope!, id);
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(`Failed to delete: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addCategory() {
|
||||||
|
if (!this.scope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showCategoryRegistryDetailDialog(this, {
|
||||||
|
scope: this.scope,
|
||||||
|
createEntry: (values) =>
|
||||||
|
createCategoryRegistryEntry(this.hass, this.scope!, values),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _categorySelected(ev: CustomEvent<SelectedDetail<number>>) {
|
||||||
|
if (!ev.detail.index) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = ev.detail.index - 1;
|
||||||
|
|
||||||
|
const val = this._categories![index]?.category_id;
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value = [val];
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: this.value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
mwc-list {
|
||||||
|
--mdc-list-item-meta-size: auto;
|
||||||
|
--mdc-list-side-padding-right: 4px;
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.add {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-categories": HaFilterCategories;
|
||||||
|
}
|
||||||
|
}
|
258
src/components/ha-filter-devices.ts
Normal file
258
src/components/ha-filter-devices.ts
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { computeDeviceName } from "../data/device_registry";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
|
@customElement("ha-filter-devices")
|
||||||
|
export class HaFilterDevices extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
public willUpdate(properties: PropertyValues) {
|
||||||
|
super.willUpdate(properties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
loadVirtualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`<search-input-outlined
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
>
|
||||||
|
</search-input-outlined>
|
||||||
|
<mwc-list class="ha-scrollbar">
|
||||||
|
<lit-virtualizer
|
||||||
|
.items=${this._devices(
|
||||||
|
this.hass.devices,
|
||||||
|
this._filter || "",
|
||||||
|
this.value
|
||||||
|
)}
|
||||||
|
.keyFunction=${this._keyFunction}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _keyFunction = (device) => device?.id;
|
||||||
|
|
||||||
|
private _renderItem = (device) =>
|
||||||
|
!device
|
||||||
|
? nothing
|
||||||
|
: html`<ha-check-list-item
|
||||||
|
.value=${device.id}
|
||||||
|
.selected=${this.value?.includes(device.id)}
|
||||||
|
>
|
||||||
|
${computeDeviceName(device, this.hass)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
|
const value = listItem?.value;
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value?.includes(value)) {
|
||||||
|
this.value = this.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
this.value = [...(this.value || []), value];
|
||||||
|
}
|
||||||
|
listItem.selected = this.value?.includes(value);
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _devices = memoizeOne(
|
||||||
|
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
||||||
|
const values = Object.values(devices);
|
||||||
|
return values
|
||||||
|
.filter(
|
||||||
|
(device) =>
|
||||||
|
!filter ||
|
||||||
|
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
computeDeviceName(a, this.hass),
|
||||||
|
computeDeviceName(b, this.hass),
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!this.value?.length) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const deviceId of this.value) {
|
||||||
|
value.push(deviceId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
ha-check-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-devices": HaFilterDevices;
|
||||||
|
}
|
||||||
|
}
|
277
src/components/ha-filter-entities.ts
Normal file
277
src/components/ha-filter-entities.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-state-icon";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
|
@customElement("ha-filter-entities")
|
||||||
|
export class HaFilterEntities extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
public willUpdate(properties: PropertyValues) {
|
||||||
|
super.willUpdate(properties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
loadVirtualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<search-input-outlined
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
>
|
||||||
|
</search-input-outlined>
|
||||||
|
<mwc-list class="ha-scrollbar">
|
||||||
|
<lit-virtualizer
|
||||||
|
.items=${this._entities(
|
||||||
|
this.hass.states,
|
||||||
|
this.type,
|
||||||
|
this._filter || "",
|
||||||
|
this.value
|
||||||
|
)}
|
||||||
|
.keyFunction=${this._keyFunction}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _keyFunction = (entity) => entity?.entity_id;
|
||||||
|
|
||||||
|
private _renderItem = (entity) =>
|
||||||
|
!entity
|
||||||
|
? nothing
|
||||||
|
: html`<ha-check-list-item
|
||||||
|
.value=${entity.entity_id}
|
||||||
|
.selected=${this.value?.includes(entity.entity_id)}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-state-icon
|
||||||
|
slot="graphic"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${entity}
|
||||||
|
></ha-state-icon>
|
||||||
|
${computeStateName(entity)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
|
const value = listItem?.value;
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value?.includes(value)) {
|
||||||
|
this.value = this.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
this.value = [...(this.value || []), value];
|
||||||
|
}
|
||||||
|
listItem.selected = this.value?.includes(value);
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entities = memoizeOne(
|
||||||
|
(
|
||||||
|
states: HomeAssistant["states"],
|
||||||
|
type: this["type"],
|
||||||
|
filter: string,
|
||||||
|
_value
|
||||||
|
) => {
|
||||||
|
const values = Object.values(states);
|
||||||
|
return values
|
||||||
|
.filter(
|
||||||
|
(entityState) =>
|
||||||
|
(!type || computeStateDomain(entityState) !== type) &&
|
||||||
|
(!filter ||
|
||||||
|
entityState.entity_id.toLowerCase().includes(filter) ||
|
||||||
|
entityState.attributes.friendly_name
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(filter))
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
computeStateName(a),
|
||||||
|
computeStateName(b),
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!this.value?.length) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const entityId of this.value) {
|
||||||
|
value.push(entityId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
ha-check-list-item {
|
||||||
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-entities": HaFilterEntities;
|
||||||
|
}
|
||||||
|
}
|
348
src/components/ha-filter-floor-areas.ts
Normal file
348
src/components/ha-filter-floor-areas.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
FloorRegistryEntry,
|
||||||
|
getFloorAreaLookup,
|
||||||
|
subscribeFloorRegistry,
|
||||||
|
} from "../data/floor_registry";
|
||||||
|
import { RelatedResult, findRelated } from "../data/search";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-floor-icon";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
|
@customElement("ha-filter-floor-areas")
|
||||||
|
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: {
|
||||||
|
floors?: string[];
|
||||||
|
areas?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const areas = this._areas(this.hass.areas, this._floors);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||||
|
${this.value?.areas?.length || this.value?.floors?.length
|
||||||
|
? html`<div class="badge">
|
||||||
|
${(this.value?.areas?.length || 0) +
|
||||||
|
(this.value?.floors?.length || 0)}
|
||||||
|
</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list class="ha-scrollbar">
|
||||||
|
${repeat(
|
||||||
|
areas?.floors || [],
|
||||||
|
(floor) => floor.floor_id,
|
||||||
|
(floor) => html`
|
||||||
|
<ha-check-list-item
|
||||||
|
.value=${floor.floor_id}
|
||||||
|
.type=${"floors"}
|
||||||
|
.selected=${this.value?.floors?.includes(
|
||||||
|
floor.floor_id
|
||||||
|
) || false}
|
||||||
|
graphic="icon"
|
||||||
|
@request-selected=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
<ha-floor-icon
|
||||||
|
slot="graphic"
|
||||||
|
.floor=${floor}
|
||||||
|
></ha-floor-icon>
|
||||||
|
${floor.name}
|
||||||
|
</ha-check-list-item>
|
||||||
|
${repeat(
|
||||||
|
floor.areas,
|
||||||
|
(area, index) =>
|
||||||
|
`${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`,
|
||||||
|
(area, index) =>
|
||||||
|
this._renderArea(area, index === floor.areas.length - 1)
|
||||||
|
)}
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${repeat(
|
||||||
|
areas?.unassisgnedAreas,
|
||||||
|
(area) => area.area_id,
|
||||||
|
(area) => this._renderArea(area)
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderArea(area, last: boolean = false) {
|
||||||
|
const hasFloor = !!area.floor_id;
|
||||||
|
return html`
|
||||||
|
<ha-check-list-item
|
||||||
|
.value=${area.area_id}
|
||||||
|
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||||
|
.type=${"areas"}
|
||||||
|
graphic="icon"
|
||||||
|
@request-selected=${this._handleItemClick}
|
||||||
|
class=${classMap({
|
||||||
|
rtl: computeRTL(this.hass),
|
||||||
|
floor: hasFloor,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${hasFloor
|
||||||
|
? html`
|
||||||
|
<ha-tree-indicator
|
||||||
|
.end=${last}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-tree-indicator>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${area.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${area.name}
|
||||||
|
</ha-check-list-item>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const listItem = ev.currentTarget;
|
||||||
|
const type = listItem?.type;
|
||||||
|
const value = listItem?.value;
|
||||||
|
|
||||||
|
if (ev.detail.selected === listItem.selected || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.[type]?.includes(value)) {
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
[type]: this.value[type].filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!this.value) {
|
||||||
|
this.value = {};
|
||||||
|
}
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
[type]: [...(this.value[type] || []), value],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listItem.selected = this.value[type]?.includes(value);
|
||||||
|
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
|
this._floors = floors;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _areas = memoizeOne(
|
||||||
|
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
||||||
|
const areas = Object.values(areaReg);
|
||||||
|
|
||||||
|
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||||
|
|
||||||
|
const unassisgnedAreas = areas.filter(
|
||||||
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
floors: floors?.map((floor) => ({
|
||||||
|
...floor,
|
||||||
|
areas: floorAreaLookup[floor.floor_id] || [],
|
||||||
|
})),
|
||||||
|
unassisgnedAreas: unassisgnedAreas,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.value ||
|
||||||
|
(!this.value.areas?.length && !this.value.floors?.length)
|
||||||
|
) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: {},
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.areas) {
|
||||||
|
for (const areaId of this.value.areas) {
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.floors) {
|
||||||
|
for (const floorId of this.value.floors) {
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: this.value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
ha-check-list-item {
|
||||||
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
|
}
|
||||||
|
.floor {
|
||||||
|
padding-left: 48px;
|
||||||
|
padding-inline-start: 48px;
|
||||||
|
padding-inline-end: 16px;
|
||||||
|
}
|
||||||
|
ha-tree-indicator {
|
||||||
|
width: 56px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
.rtl ha-tree-indicator {
|
||||||
|
right: 0px;
|
||||||
|
left: initial;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
.subdir {
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
.
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-floor-areas": HaFilterFloorAreas;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"data-table-filter-changed": { value: any; items: Set<string> | undefined };
|
||||||
|
}
|
||||||
|
}
|
231
src/components/ha-filter-integrations.ts
Normal file
231
src/components/ha-filter-integrations.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import {
|
||||||
|
fetchIntegrationManifests,
|
||||||
|
IntegrationManifest,
|
||||||
|
} from "../data/integration";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-domain-icon";
|
||||||
|
import "./search-input-outlined";
|
||||||
|
|
||||||
|
@customElement("ha-filter-integrations")
|
||||||
|
export class HaFilterIntegrations extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _manifests?: IntegrationManifest[];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._manifests && this._shouldRender
|
||||||
|
? html`<search-input-outlined
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._handleSearchChange}
|
||||||
|
>
|
||||||
|
</search-input-outlined>
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._integrationsSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${repeat(
|
||||||
|
this._integrations(this._manifests, this._filter, this.value),
|
||||||
|
(i) => i.domain,
|
||||||
|
(integration) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${integration.domain}
|
||||||
|
.selected=${(this.value || []).includes(
|
||||||
|
integration.domain
|
||||||
|
)}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-domain-icon
|
||||||
|
slot="graphic"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${integration.domain}
|
||||||
|
brandFallback
|
||||||
|
></ha-domain-icon>
|
||||||
|
${integration.name || integration.domain}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list> `
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async firstUpdated() {
|
||||||
|
this._manifests = await fetchIntegrationManifests(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _integrations = memoizeOne(
|
||||||
|
(manifest: IntegrationManifest[], filter: string | undefined, _value) =>
|
||||||
|
manifest
|
||||||
|
.filter(
|
||||||
|
(mnfst) =>
|
||||||
|
(!mnfst.integration_type ||
|
||||||
|
!["entity", "system", "hardware"].includes(
|
||||||
|
mnfst.integration_type
|
||||||
|
)) &&
|
||||||
|
(!filter ||
|
||||||
|
mnfst.name.toLowerCase().includes(filter) ||
|
||||||
|
mnfst.domain.toLowerCase().includes(filter))
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
a.name || a.domain,
|
||||||
|
b.name || b.domain,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _integrationsSelected(
|
||||||
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
|
) {
|
||||||
|
const integrations = this._integrations(
|
||||||
|
this._manifests!,
|
||||||
|
this._filter,
|
||||||
|
this.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const domain = integrations[index].domain;
|
||||||
|
value.push(domain);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
display: block;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-integrations": HaFilterIntegrations;
|
||||||
|
}
|
||||||
|
}
|
228
src/components/ha-filter-labels.ts
Normal file
228
src/components/ha-filter-labels.ts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { navigate } from "../common/navigate";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-label";
|
||||||
|
|
||||||
|
@customElement("ha-filter-labels")
|
||||||
|
export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _labels: LabelRegistryEntry[] = [];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._labelSelected}
|
||||||
|
class="ha-scrollbar"
|
||||||
|
multi
|
||||||
|
>
|
||||||
|
${repeat(
|
||||||
|
this._labels,
|
||||||
|
(label) => label.label_id,
|
||||||
|
(label) => {
|
||||||
|
const color = label.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
return html`<ha-check-list-item
|
||||||
|
.value=${label.label_id}
|
||||||
|
.selected=${(this.value || []).includes(label.label_id)}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||||
|
${label.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="icon"
|
||||||
|
.icon=${label.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${label.name}
|
||||||
|
</ha-label>
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
${this.expanded
|
||||||
|
? html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
@click=${this._manageLabels}
|
||||||
|
class="add"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
||||||
|
</ha-list-item>`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - (49 + 48)}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _manageLabels() {
|
||||||
|
navigate("/config/labels");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const labelId = this._labels[index].label_id;
|
||||||
|
value.push(labelId);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
ha-label {
|
||||||
|
--ha-label-background-color: var(--color, var(--grey-color));
|
||||||
|
--ha-label-background-opacity: 0.5;
|
||||||
|
}
|
||||||
|
.add {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-labels": HaFilterLabels;
|
||||||
|
}
|
||||||
|
}
|
183
src/components/ha-filter-states.ts
Normal file
183
src/components/ha-filter-states.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-icon";
|
||||||
|
|
||||||
|
@customElement("ha-filter-states")
|
||||||
|
export class HaFilterStates extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public states?: {
|
||||||
|
value: any;
|
||||||
|
label?: string;
|
||||||
|
icon?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.states) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const hasIcon = this.states.find((item) => item.icon);
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.label}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterVariantRemove}
|
||||||
|
@click=${this._clearFilter}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._statesSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${this.states.map(
|
||||||
|
(item) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${item.value}
|
||||||
|
.selected=${this.value?.includes(item.value)}
|
||||||
|
.graphic=${hasIcon ? "icon" : undefined}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${item.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${item.label}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _statesSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const val = this.states![index].value;
|
||||||
|
value.push(val);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.value = undefined;
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: undefined,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header ha-icon-button {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-states": HaFilterStates;
|
||||||
|
}
|
||||||
|
}
|
56
src/components/ha-floor-icon.ts
Normal file
56
src/components/ha-floor-icon.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
mdiHome,
|
||||||
|
mdiHomeFloor0,
|
||||||
|
mdiHomeFloor1,
|
||||||
|
mdiHomeFloor2,
|
||||||
|
mdiHomeFloor3,
|
||||||
|
mdiHomeFloorNegative1,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { LitElement, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { FloorRegistryEntry } from "../data/floor_registry";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
export const floorDefaultIconPath = (
|
||||||
|
floor: Pick<FloorRegistryEntry, "level">
|
||||||
|
) => {
|
||||||
|
switch (floor.level) {
|
||||||
|
case 0:
|
||||||
|
return mdiHomeFloor0;
|
||||||
|
case 1:
|
||||||
|
return mdiHomeFloor1;
|
||||||
|
case 2:
|
||||||
|
return mdiHomeFloor2;
|
||||||
|
case 3:
|
||||||
|
return mdiHomeFloor3;
|
||||||
|
case -1:
|
||||||
|
return mdiHomeFloorNegative1;
|
||||||
|
}
|
||||||
|
return mdiHome;
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-floor-icon")
|
||||||
|
export class HaFloorIcon extends LitElement {
|
||||||
|
@property({ attribute: false }) public floor!: Pick<
|
||||||
|
FloorRegistryEntry,
|
||||||
|
"icon" | "level"
|
||||||
|
>;
|
||||||
|
|
||||||
|
@property() public icon?: string;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this.floor.icon) {
|
||||||
|
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
||||||
|
}
|
||||||
|
const defaultPath = floorDefaultIconPath(this.floor);
|
||||||
|
|
||||||
|
return html`<ha-svg-icon .path=${defaultPath}></ha-svg-icon>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-floor-icon": HaFloorIcon;
|
||||||
|
}
|
||||||
|
}
|
500
src/components/ha-floor-picker.ts
Normal file
500
src/components/ha-floor-picker.ts
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import {
|
||||||
|
ScorableTextItem,
|
||||||
|
fuzzyFilterSort,
|
||||||
|
} from "../common/string/filter/sequence-matching";
|
||||||
|
import {
|
||||||
|
AreaRegistryEntry,
|
||||||
|
updateAreaRegistryEntry,
|
||||||
|
} from "../data/area_registry";
|
||||||
|
import {
|
||||||
|
DeviceEntityDisplayLookup,
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
} from "../data/device_registry";
|
||||||
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
|
import {
|
||||||
|
FloorRegistryEntry,
|
||||||
|
createFloorRegistryEntry,
|
||||||
|
getFloorAreaLookup,
|
||||||
|
subscribeFloorRegistry,
|
||||||
|
} from "../data/floor_registry";
|
||||||
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
|
import "./ha-floor-icon";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-list-item";
|
||||||
|
|
||||||
|
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
|
||||||
|
|
||||||
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
const NO_FLOORS_ID = "___NO_FLOORS___";
|
||||||
|
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
|
||||||
|
>
|
||||||
|
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
|
@customElement("ha-floor-picker")
|
||||||
|
export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only floors with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no floors with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only floors with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of floors to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-floors
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-floor" })
|
||||||
|
public excludeFloors?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _suggestion?: string;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
|
this._floors = floors;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getFloors = memoizeOne(
|
||||||
|
(
|
||||||
|
floors: FloorRegistryEntry[],
|
||||||
|
areas: AreaRegistryEntry[],
|
||||||
|
devices: DeviceRegistryEntry[],
|
||||||
|
entities: EntityRegistryDisplayEntry[],
|
||||||
|
includeDomains: this["includeDomains"],
|
||||||
|
excludeDomains: this["excludeDomains"],
|
||||||
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
|
deviceFilter: this["deviceFilter"],
|
||||||
|
entityFilter: this["entityFilter"],
|
||||||
|
noAdd: this["noAdd"],
|
||||||
|
excludeFloors: this["excludeFloors"]
|
||||||
|
): FloorRegistryEntry[] => {
|
||||||
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
|
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
includeDomains ||
|
||||||
|
excludeDomains ||
|
||||||
|
includeDeviceClasses ||
|
||||||
|
deviceFilter ||
|
||||||
|
entityFilter
|
||||||
|
) {
|
||||||
|
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||||
|
inputDevices = devices;
|
||||||
|
inputEntities = entities.filter((entity) => entity.area_id);
|
||||||
|
|
||||||
|
if (includeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entities.every(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) =>
|
||||||
|
deviceFilter!(device)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter!(stateObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputFloors = floors;
|
||||||
|
|
||||||
|
let areaIds: string[] | undefined;
|
||||||
|
|
||||||
|
if (inputDevices) {
|
||||||
|
areaIds = inputDevices
|
||||||
|
.filter((device) => device.area_id)
|
||||||
|
.map((device) => device.area_id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputEntities) {
|
||||||
|
areaIds = (areaIds ?? []).concat(
|
||||||
|
inputEntities
|
||||||
|
.filter((entity) => entity.area_id)
|
||||||
|
.map((entity) => entity.area_id!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds) {
|
||||||
|
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||||
|
outputFloors = outputFloors.filter((floor) =>
|
||||||
|
floorAreaLookup[floor.floor_id]?.some((area) =>
|
||||||
|
areaIds!.includes(area.area_id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeFloors) {
|
||||||
|
outputFloors = outputFloors.filter(
|
||||||
|
(floor) => !excludeFloors!.includes(floor.floor_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputFloors.length) {
|
||||||
|
outputFloors = [
|
||||||
|
{
|
||||||
|
floor_id: NO_FLOORS_ID,
|
||||||
|
name: this.hass.localize("ui.components.floor-picker.no_floors"),
|
||||||
|
icon: null,
|
||||||
|
level: null,
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return noAdd
|
||||||
|
? outputFloors
|
||||||
|
: [
|
||||||
|
...outputFloors,
|
||||||
|
{
|
||||||
|
floor_id: ADD_NEW_ID,
|
||||||
|
name: this.hass.localize("ui.components.floor-picker.add_new"),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
level: null,
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass && this._floors) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const floors = this._getFloors(
|
||||||
|
this._floors!,
|
||||||
|
Object.values(this.hass.areas),
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeFloors
|
||||||
|
).map((floor) => ({
|
||||||
|
...floor,
|
||||||
|
strings: [floor.floor_id, floor.name, ...floor.aliases],
|
||||||
|
}));
|
||||||
|
this.comboBox.items = floors;
|
||||||
|
this.comboBox.filteredItems = floors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
item-value-path="floor_id"
|
||||||
|
item-id-path="floor_id"
|
||||||
|
item-label-path="name"
|
||||||
|
.value=${this._value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.floor-picker.floor")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder
|
||||||
|
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
|
||||||
|
?.name
|
||||||
|
: undefined}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._floorChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = fuzzyFilterSort<ScorableFloorRegistryEntry>(
|
||||||
|
filterString,
|
||||||
|
target.items?.filter(
|
||||||
|
(item) => ![NO_FLOORS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
if (filteredItems.length === 0) {
|
||||||
|
if (this.noAdd) {
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
floor_id: NO_FLOORS_ID,
|
||||||
|
name: this.hass.localize("ui.components.floor-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
level: null,
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
] as FloorRegistryEntry[];
|
||||||
|
} else {
|
||||||
|
this._suggestion = filterString;
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
floor_id: ADD_NEW_SUGGESTION_ID,
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.floor-picker.add_new_sugestion",
|
||||||
|
{ name: this._suggestion }
|
||||||
|
),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
level: null,
|
||||||
|
aliases: [],
|
||||||
|
},
|
||||||
|
] as FloorRegistryEntry[];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _floorChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
let newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === NO_FLOORS_ID) {
|
||||||
|
newValue = "";
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
|
showFloorRegistryDetailDialog(this, {
|
||||||
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
|
createEntry: async (values, addedAreas) => {
|
||||||
|
try {
|
||||||
|
const floor = await createFloorRegistryEntry(this.hass, values);
|
||||||
|
addedAreas.forEach((areaId) => {
|
||||||
|
updateAreaRegistryEntry(this.hass, areaId, {
|
||||||
|
floor_id: floor.floor_id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const floors = [...this._floors!, floor];
|
||||||
|
this.comboBox.filteredItems = this._getFloors(
|
||||||
|
floors,
|
||||||
|
Object.values(this.hass.areas)!,
|
||||||
|
Object.values(this.hass.devices)!,
|
||||||
|
Object.values(this.hass.entities)!,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeFloors
|
||||||
|
);
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox.updateComplete;
|
||||||
|
this._setValue(floor.floor_id);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.components.floor-picker.failed_create_floor"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._suggestion = undefined;
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-floor-picker": HaFloorPicker;
|
||||||
|
}
|
||||||
|
}
|
169
src/components/ha-floors-picker.ts
Normal file
169
src/components/ha-floors-picker.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-floor-picker";
|
||||||
|
|
||||||
|
@customElement("ha-floors-picker")
|
||||||
|
export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Array }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only floors with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no floors with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only floors with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ attribute: "picked-floor-label" })
|
||||||
|
public pickedFloorLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "pick-floor-label" })
|
||||||
|
public pickFloorLabel?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFloors = this._currentFloors;
|
||||||
|
return html`
|
||||||
|
${currentFloors.map(
|
||||||
|
(floor) => html`
|
||||||
|
<div>
|
||||||
|
<ha-floor-picker
|
||||||
|
.curValue=${floor}
|
||||||
|
.noAdd=${this.noAdd}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${floor}
|
||||||
|
.label=${this.pickedFloorLabel}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDomains=${this.excludeDomains}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@value-changed=${this._floorChanged}
|
||||||
|
></ha-floor-picker>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<ha-floor-picker
|
||||||
|
.noAdd=${this.noAdd}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.label=${this.pickFloorLabel}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeDomains=${this.excludeDomains}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.required=${this.required && !currentFloors.length}
|
||||||
|
@value-changed=${this._addFloor}
|
||||||
|
.excludeFloors=${currentFloors}
|
||||||
|
></ha-floor-picker>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _currentFloors(): string[] {
|
||||||
|
return this.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateFloors(floors) {
|
||||||
|
this.value = floors;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: floors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _floorChanged(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const curValue = (ev.currentTarget as any).curValue;
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (newValue === curValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentFloors = this._currentFloors;
|
||||||
|
if (!newValue || currentFloors.includes(newValue)) {
|
||||||
|
this._updateFloors(currentFloors.filter((ent) => ent !== curValue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._updateFloors(
|
||||||
|
currentFloors.map((ent) => (ent === curValue ? newValue : ent))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addFloor(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const toAdd = ev.detail.value;
|
||||||
|
if (!toAdd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(ev.currentTarget as any).value = "";
|
||||||
|
const currentFloors = this._currentFloors;
|
||||||
|
if (currentFloors.includes(toAdd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateFloors([...currentFloors, toAdd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static override styles = css`
|
||||||
|
div {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-floors-picker": HaFloorsPicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -118,7 +118,7 @@ export class HaIconPicker extends LitElement {
|
|||||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||||
</ha-icon>
|
</ha-icon>
|
||||||
`
|
`
|
||||||
: html`<slot name="fallback"></slot>`}
|
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||||
</ha-combo-box>
|
</ha-combo-box>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
493
src/components/ha-label-picker.ts
Normal file
493
src/components/ha-label-picker.ts
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import {
|
||||||
|
ScorableTextItem,
|
||||||
|
fuzzyFilterSort,
|
||||||
|
} from "../common/string/filter/sequence-matching";
|
||||||
|
import {
|
||||||
|
DeviceEntityDisplayLookup,
|
||||||
|
DeviceRegistryEntry,
|
||||||
|
getDeviceEntityDisplayLookup,
|
||||||
|
} from "../data/device_registry";
|
||||||
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-combo-box";
|
||||||
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-list-item";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
|
||||||
|
|
||||||
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
const NO_LABELS_ID = "___NO_LABELS___";
|
||||||
|
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
|
@customElement("ha-label-picker")
|
||||||
|
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no labels with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of labels to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-labels
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-label" })
|
||||||
|
public excludeLabels?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
private _suggestion?: string;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLabels = memoizeOne(
|
||||||
|
(
|
||||||
|
labels: LabelRegistryEntry[],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
|
devices: DeviceRegistryEntry[],
|
||||||
|
entities: EntityRegistryDisplayEntry[],
|
||||||
|
includeDomains: this["includeDomains"],
|
||||||
|
excludeDomains: this["excludeDomains"],
|
||||||
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
|
deviceFilter: this["deviceFilter"],
|
||||||
|
entityFilter: this["entityFilter"],
|
||||||
|
noAdd: this["noAdd"],
|
||||||
|
excludeLabels: this["excludeLabels"]
|
||||||
|
): LabelRegistryEntry[] => {
|
||||||
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||||
|
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
includeDomains ||
|
||||||
|
excludeDomains ||
|
||||||
|
includeDeviceClasses ||
|
||||||
|
deviceFilter ||
|
||||||
|
entityFilter
|
||||||
|
) {
|
||||||
|
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||||
|
inputDevices = devices;
|
||||||
|
inputEntities = entities.filter((entity) => entity.labels.length > 0);
|
||||||
|
|
||||||
|
if (includeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) =>
|
||||||
|
includeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeDomains) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entities.every(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter(
|
||||||
|
(entity) =>
|
||||||
|
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeDeviceClasses) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
return (
|
||||||
|
stateObj.attributes.device_class &&
|
||||||
|
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) =>
|
||||||
|
deviceFilter!(device)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityFilter) {
|
||||||
|
inputDevices = inputDevices!.filter((device) => {
|
||||||
|
const devEntities = deviceEntityLookup[device.id];
|
||||||
|
if (!devEntities || !devEntities.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return deviceEntityLookup[device.id].some((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter(stateObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
inputEntities = inputEntities!.filter((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
if (!stateObj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return entityFilter!(stateObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputLabels = labels;
|
||||||
|
const usedLabels = new Set<string>();
|
||||||
|
|
||||||
|
let areaIds: string[] | undefined;
|
||||||
|
|
||||||
|
if (inputDevices) {
|
||||||
|
areaIds = inputDevices
|
||||||
|
.filter((device) => device.area_id)
|
||||||
|
.map((device) => device.area_id!);
|
||||||
|
|
||||||
|
inputDevices.forEach((device) => {
|
||||||
|
device.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputEntities) {
|
||||||
|
areaIds = (areaIds ?? []).concat(
|
||||||
|
inputEntities
|
||||||
|
.filter((entity) => entity.area_id)
|
||||||
|
.map((entity) => entity.area_id!)
|
||||||
|
);
|
||||||
|
inputEntities.forEach((entity) => {
|
||||||
|
entity.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaIds) {
|
||||||
|
areaIds.forEach((areaId) => {
|
||||||
|
const area = areas[areaId];
|
||||||
|
area.labels.forEach((label) => usedLabels.add(label));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeLabels) {
|
||||||
|
outputLabels = outputLabels.filter(
|
||||||
|
(label) => !excludeLabels!.includes(label.label_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputDevices || inputEntities) {
|
||||||
|
outputLabels = outputLabels.filter((label) =>
|
||||||
|
usedLabels.has(label.label_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputLabels.length) {
|
||||||
|
outputLabels = [
|
||||||
|
{
|
||||||
|
label_id: NO_LABELS_ID,
|
||||||
|
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return noAdd
|
||||||
|
? outputLabels
|
||||||
|
: [
|
||||||
|
...outputLabels,
|
||||||
|
{
|
||||||
|
label_id: ADD_NEW_ID,
|
||||||
|
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass && this._labels) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const items = this._getLabels(
|
||||||
|
this._labels!,
|
||||||
|
this.hass.areas,
|
||||||
|
Object.values(this.hass.devices),
|
||||||
|
Object.values(this.hass.entities),
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeLabels
|
||||||
|
).map((label) => ({
|
||||||
|
...label,
|
||||||
|
strings: [label.label_id, label.name],
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.comboBox.items = items;
|
||||||
|
this.comboBox.filteredItems = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
item-value-path="label_id"
|
||||||
|
item-id-path="label_id"
|
||||||
|
item-label-path="name"
|
||||||
|
.value=${this._value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.label-picker.label")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder
|
||||||
|
? this._labels?.find((label) => label.label_id === this.placeholder)
|
||||||
|
?.name
|
||||||
|
: undefined}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._labelChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = fuzzyFilterSort<ScorableLabelItem>(
|
||||||
|
filterString,
|
||||||
|
target.items?.filter(
|
||||||
|
(item) => ![NO_LABELS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
if (filteredItems.length === 0) {
|
||||||
|
if (this.noAdd) {
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
label_id: NO_LABELS_ID,
|
||||||
|
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||||
|
icon: null,
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
] as ScorableLabelItem[];
|
||||||
|
} else {
|
||||||
|
this._suggestion = filterString;
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
label_id: ADD_NEW_SUGGESTION_ID,
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.label-picker.add_new_sugestion",
|
||||||
|
{ name: this._suggestion }
|
||||||
|
),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
color: null,
|
||||||
|
},
|
||||||
|
] as ScorableLabelItem[];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
let newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === NO_LABELS_ID) {
|
||||||
|
newValue = "";
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
|
|
||||||
|
showLabelDetailDialog(this, {
|
||||||
|
entry: undefined,
|
||||||
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
|
createEntry: async (values) => {
|
||||||
|
const label = await createLabelRegistryEntry(this.hass, values);
|
||||||
|
const labels = [...this._labels!, label];
|
||||||
|
this.comboBox.filteredItems = this._getLabels(
|
||||||
|
labels,
|
||||||
|
this.hass.areas!,
|
||||||
|
Object.values(this.hass.devices)!,
|
||||||
|
Object.values(this.hass.entities)!,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.noAdd,
|
||||||
|
this.excludeLabels
|
||||||
|
);
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox.updateComplete;
|
||||||
|
this._setValue(label.label_id);
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._suggestion = undefined;
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-label-picker": HaLabelPicker;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,17 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "@material/web/ripple/ripple";
|
||||||
|
|
||||||
@customElement("ha-label")
|
@customElement("ha-label")
|
||||||
class HaLabel extends LitElement {
|
class HaLabel extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true }) dense = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<span class="label">
|
<span class="content">
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
<md-ripple></md-ripple>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -22,8 +26,10 @@ class HaLabel extends LitElement {
|
|||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
}
|
--ha-label-background-opacity: 1;
|
||||||
.label {
|
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -35,9 +41,23 @@ class HaLabel extends LitElement {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background-color: var(--ha-label-background-color);
|
|
||||||
color: var(--ha-label-text-color);
|
color: var(--ha-label-text-color);
|
||||||
--mdc-icon-size: 18px;
|
--mdc-icon-size: 12px;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.content > * {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:host:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background-color: var(--ha-label-background-color);
|
||||||
|
opacity: var(--ha-label-background-opacity);
|
||||||
}
|
}
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]) {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
@@ -45,11 +65,23 @@ class HaLabel extends LitElement {
|
|||||||
margin-inline-start: -8px;
|
margin-inline-start: -8px;
|
||||||
margin-inline-end: 8px;
|
margin-inline-end: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--ha-label-icon-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host([dense]) {
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
:host([dense]) ::slotted([slot="icon"]) {
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-inline-start: -4px;
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
227
src/components/ha-labels-picker.ts
Normal file
227
src/components/ha-labels-picker.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
updateLabelRegistryEntry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import "./chips/ha-chip-set";
|
||||||
|
import "./chips/ha-input-chip";
|
||||||
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
|
import "./ha-label-picker";
|
||||||
|
import type { HaLabelPicker } from "./ha-label-picker";
|
||||||
|
|
||||||
|
@customElement("ha-labels-picker")
|
||||||
|
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities from specific domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-domains" })
|
||||||
|
public includeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show no labels with entities of these domains.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-domains
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-domains" })
|
||||||
|
public excludeDomains?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show only labels with entities of these device classes.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr include-device-classes
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "include-device-classes" })
|
||||||
|
public includeDeviceClasses?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of labels to be excluded.
|
||||||
|
* @type {Array}
|
||||||
|
* @attr exclude-labels
|
||||||
|
*/
|
||||||
|
@property({ type: Array, attribute: "exclude-label" })
|
||||||
|
public excludeLabels?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public entityFilter?: (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _labels?: { [id: string]: LabelRegistryEntry };
|
||||||
|
|
||||||
|
@query("ha-label-picker", true) public labelPicker!: HaLabelPicker;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.labelPicker?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.labelPicker?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
const lookUp = {};
|
||||||
|
labels.forEach((label) => {
|
||||||
|
lookUp[label.label_id] = label;
|
||||||
|
});
|
||||||
|
this._labels = lookUp;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sortedLabels = memoizeOne(
|
||||||
|
(
|
||||||
|
value: string[] | undefined,
|
||||||
|
labels: { [id: string]: LabelRegistryEntry } | undefined,
|
||||||
|
language: string
|
||||||
|
) =>
|
||||||
|
value
|
||||||
|
?.map((id) => labels?.[id])
|
||||||
|
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const labels = this._sortedLabels(
|
||||||
|
this.value,
|
||||||
|
this._labels,
|
||||||
|
this.hass.locale.language
|
||||||
|
);
|
||||||
|
return html`
|
||||||
|
${labels?.length
|
||||||
|
? html`<ha-chip-set>
|
||||||
|
${repeat(
|
||||||
|
labels,
|
||||||
|
(label) => label?.label_id,
|
||||||
|
(label) => {
|
||||||
|
const color = label?.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
return html`
|
||||||
|
<ha-input-chip
|
||||||
|
.item=${label}
|
||||||
|
@remove=${this._removeItem}
|
||||||
|
@click=${this._openDetail}
|
||||||
|
.label=${label?.name}
|
||||||
|
selected
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
>
|
||||||
|
${label?.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="icon"
|
||||||
|
.icon=${label.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-input-chip>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</ha-chip-set>`
|
||||||
|
: nothing}
|
||||||
|
<ha-label-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.label-picker.add_label")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.excludeLabels=${this.value}
|
||||||
|
@value-changed=${this._labelChanged}
|
||||||
|
>
|
||||||
|
</ha-label-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeItem(ev) {
|
||||||
|
const label = ev.currentTarget.item;
|
||||||
|
this._setValue(this._value.filter((id) => id !== label.label_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openDetail(ev) {
|
||||||
|
const label = ev.currentTarget.item;
|
||||||
|
showLabelDetailDialog(this, {
|
||||||
|
entry: label,
|
||||||
|
updateEntry: async (values) => {
|
||||||
|
const updated = await updateLabelRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
label.label_id,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
if (!newValue || this._value.includes(newValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setValue([...this._value, newValue]);
|
||||||
|
this.labelPicker.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: string[]) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
ha-chip-set {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
ha-input-chip {
|
||||||
|
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||||
|
--ha-input-chip-selected-container-opacity: 0.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-labels-picker": HaLabelsPicker;
|
||||||
|
}
|
||||||
|
}
|
44
src/components/ha-menu-item.ts
Normal file
44
src/components/ha-menu-item.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||||
|
import "element-internals-polyfill";
|
||||||
|
import { CSSResult, css } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-menu-item")
|
||||||
|
export class HaMenuItem extends MdMenuItem {
|
||||||
|
static override styles: CSSResult[] = [
|
||||||
|
...MdMenuItem.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--ha-icon-display: block;
|
||||||
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-primary: var(--primary-text-color);
|
||||||
|
--md-sys-color-secondary: var(--secondary-text-color);
|
||||||
|
--md-sys-color-surface: var(--card-background-color);
|
||||||
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||||
|
--md-sys-color-secondary-container: rgba(
|
||||||
|
var(--rgb-primary-color),
|
||||||
|
0.15
|
||||||
|
);
|
||||||
|
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||||
|
--mdc-icon-size: 16px;
|
||||||
|
|
||||||
|
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
:host(.warning) {
|
||||||
|
--md-menu-item-label-text-color: var(--error-color);
|
||||||
|
--md-menu-item-leading-icon-color: var(--error-color);
|
||||||
|
}
|
||||||
|
::slotted([slot="headline"]) {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-menu-item": HaMenuItem;
|
||||||
|
}
|
||||||
|
}
|
22
src/components/ha-menu.ts
Normal file
22
src/components/ha-menu.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "element-internals-polyfill";
|
||||||
|
import { CSSResult, css } from "lit";
|
||||||
|
import { MdMenu } from "@material/web/menu/menu";
|
||||||
|
|
||||||
|
@customElement("ha-menu")
|
||||||
|
export class HaMenu extends MdMenu {
|
||||||
|
static override styles: CSSResult[] = [
|
||||||
|
...MdMenu.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--md-sys-color-surface-container: var(--card-background-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-menu": HaMenu;
|
||||||
|
}
|
||||||
|
}
|
49
src/components/ha-outlined-text-field.ts
Normal file
49
src/components/ha-outlined-text-field.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field";
|
||||||
|
import "element-internals-polyfill";
|
||||||
|
import { css } from "lit";
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-outlined-text-field")
|
||||||
|
export class HaOutlinedTextField extends MdOutlinedTextField {
|
||||||
|
static override styles = [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
|
--md-outlined-text-field-input-text-color: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||||
|
--md-outlined-field-outline-color: var(--outline-color);
|
||||||
|
--md-outlined-field-focus-outline-color: var(--primary-color);
|
||||||
|
--md-outlined-field-hover-outline-color: var(--outline-hover-color);
|
||||||
|
}
|
||||||
|
:host([dense]) {
|
||||||
|
--md-outlined-field-top-space: 5.5px;
|
||||||
|
--md-outlined-field-bottom-space: 5.5px;
|
||||||
|
--md-outlined-field-container-shape-start-start: 10px;
|
||||||
|
--md-outlined-field-container-shape-start-end: 10px;
|
||||||
|
--md-outlined-field-container-shape-end-end: 10px;
|
||||||
|
--md-outlined-field-container-shape-end-start: 10px;
|
||||||
|
--md-outlined-field-focus-outline-width: 1px;
|
||||||
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
|
}
|
||||||
|
md-outlined-field {
|
||||||
|
background: var(--ha-outlined-text-field-container-color, transparent);
|
||||||
|
opacity: var(--ha-outlined-text-field-container-opacity, 1);
|
||||||
|
border-start-start-radius: var(--_container-shape-start-start);
|
||||||
|
border-start-end-radius: var(--_container-shape-start-end);
|
||||||
|
border-end-end-radius: var(--_container-shape-end-end);
|
||||||
|
border-end-start-radius: var(--_container-shape-end-start);
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-outlined-text-field": HaOutlinedTextField;
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,7 @@ export const pushSupported =
|
|||||||
class HaPushNotificationsToggle extends LitElement {
|
class HaPushNotificationsToggle extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _disabled: boolean = false;
|
@property({ type: Boolean }) public disabled!: boolean;
|
||||||
|
|
||||||
@state() private _pushChecked: boolean =
|
@state() private _pushChecked: boolean =
|
||||||
"Notification" in window && Notification.permission === "granted";
|
"Notification" in window && Notification.permission === "granted";
|
||||||
@@ -27,7 +27,7 @@ class HaPushNotificationsToggle extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<ha-switch
|
<ha-switch
|
||||||
.disabled=${this._disabled || this._loading}
|
.disabled=${this.disabled || this._loading}
|
||||||
.checked=${this._pushChecked}
|
.checked=${this._pushChecked}
|
||||||
@change=${this._handlePushChange}
|
@change=${this._handlePushChange}
|
||||||
></ha-switch>
|
></ha-switch>
|
||||||
|
@@ -87,8 +87,12 @@ export class HaAreaSelector extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
no-add
|
no-add
|
||||||
.deviceFilter=${this._filterDevices}
|
.deviceFilter=${this.selector.area?.device
|
||||||
.entityFilter=${this._filterEntities}
|
? this._filterDevices
|
||||||
|
: undefined}
|
||||||
|
.entityFilter=${this.selector.area?.entity
|
||||||
|
? this._filterEntities
|
||||||
|
: undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></ha-area-picker>
|
></ha-area-picker>
|
||||||
@@ -102,8 +106,12 @@ export class HaAreaSelector extends LitElement {
|
|||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.pickAreaLabel=${this.label}
|
.pickAreaLabel=${this.label}
|
||||||
no-add
|
no-add
|
||||||
.deviceFilter=${this._filterDevices}
|
.deviceFilter=${this.selector.area?.device
|
||||||
.entityFilter=${this._filterEntities}
|
? this._filterDevices
|
||||||
|
: undefined}
|
||||||
|
.entityFilter=${this.selector.area?.entity
|
||||||
|
? this._filterEntities
|
||||||
|
: undefined}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
></ha-areas-picker>
|
></ha-areas-picker>
|
||||||
|
153
src/components/ha-selector/ha-selector-floor.ts
Normal file
153
src/components/ha-selector/ha-selector-floor.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||||
|
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
EntitySources,
|
||||||
|
fetchEntitySourcesWithCache,
|
||||||
|
} from "../../data/entity_sources";
|
||||||
|
import type { FloorSelector } from "../../data/selector";
|
||||||
|
import {
|
||||||
|
filterSelectorDevices,
|
||||||
|
filterSelectorEntities,
|
||||||
|
} from "../../data/selector";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-floor-picker";
|
||||||
|
import "../ha-floors-picker";
|
||||||
|
|
||||||
|
@customElement("ha-selector-floor")
|
||||||
|
export class HaFloorSelector extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public selector!: FloorSelector;
|
||||||
|
|
||||||
|
@property() public value?: any;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
@state() private _entitySources?: EntitySources;
|
||||||
|
|
||||||
|
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||||
|
|
||||||
|
private _hasIntegration(selector: FloorSelector) {
|
||||||
|
return (
|
||||||
|
(selector.floor?.entity &&
|
||||||
|
ensureArray(selector.floor.entity).some(
|
||||||
|
(filter) => filter.integration
|
||||||
|
)) ||
|
||||||
|
(selector.floor?.device &&
|
||||||
|
ensureArray(selector.floor.device).some((device) => device.integration))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||||
|
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
||||||
|
this.value = [this.value];
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
} else if (!this.selector.floor?.multiple && Array.isArray(this.value)) {
|
||||||
|
this.value = this.value[0];
|
||||||
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
|
if (
|
||||||
|
changedProperties.has("selector") &&
|
||||||
|
this._hasIntegration(this.selector) &&
|
||||||
|
!this._entitySources
|
||||||
|
) {
|
||||||
|
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||||
|
this._entitySources = sources;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selector.floor?.multiple) {
|
||||||
|
return html`
|
||||||
|
<ha-floor-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
|
no-add
|
||||||
|
.deviceFilter=${this.selector.floor?.device
|
||||||
|
? this._filterDevices
|
||||||
|
: undefined}
|
||||||
|
.entityFilter=${this.selector.floor?.entity
|
||||||
|
? this._filterEntities
|
||||||
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
></ha-floor-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-floors-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.pickFloorLabel=${this.label}
|
||||||
|
no-add
|
||||||
|
.deviceFilter=${this.selector.floor?.device
|
||||||
|
? this._filterDevices
|
||||||
|
: undefined}
|
||||||
|
.entityFilter=${this.selector.floor?.entity
|
||||||
|
? this._filterEntities
|
||||||
|
: undefined}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
></ha-floors-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterEntities = (entity: HassEntity): boolean => {
|
||||||
|
if (!this.selector.floor?.entity) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ensureArray(this.selector.floor.entity).some((filter) =>
|
||||||
|
filterSelectorEntities(filter, entity, this._entitySources)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||||
|
if (!this.selector.floor?.device) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceIntegrations = this._entitySources
|
||||||
|
? this._deviceIntegrationLookup(
|
||||||
|
this._entitySources,
|
||||||
|
Object.values(this.hass.entities)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return ensureArray(this.selector.floor.device).some((filter) =>
|
||||||
|
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-selector-floor": HaFloorSelector;
|
||||||
|
}
|
||||||
|
}
|
85
src/components/ha-selector/ha-selector-label.ts
Normal file
85
src/components/ha-selector/ha-selector-label.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { LabelSelector } from "../../data/selector";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "../ha-labels-picker";
|
||||||
|
|
||||||
|
@customElement("ha-selector-label")
|
||||||
|
export class HaLabelSelector extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public value?: string | string[];
|
||||||
|
|
||||||
|
@property() public name?: string;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public selector!: LabelSelector;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = true;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this.selector.label.multiple) {
|
||||||
|
return html`
|
||||||
|
<ha-labels-picker
|
||||||
|
no-add
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${ensureArray(this.value ?? [])}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
|
@value-changed=${this._handleChange}
|
||||||
|
>
|
||||||
|
</ha-labels-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-label-picker
|
||||||
|
no-add
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this.value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.label}
|
||||||
|
@value-changed=${this._handleChange}
|
||||||
|
>
|
||||||
|
</ha-label-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleChange(ev) {
|
||||||
|
let value = ev.detail.value;
|
||||||
|
if (this.value === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(value === "" || (Array.isArray(value) && value.length === 0)) &&
|
||||||
|
!this.required
|
||||||
|
) {
|
||||||
|
value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-labels-picker {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-selector-label": HaLabelSelector;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { UiColorSelector } from "../../data/selector";
|
import { UiColorSelector } from "../../data/selector";
|
||||||
import "../../panels/lovelace/components/hui-color-picker";
|
import "../ha-color-picker";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
@customElement("ha-selector-ui_color")
|
@customElement("ha-selector-ui_color")
|
||||||
@@ -19,13 +19,14 @@ export class HaSelectorUiColor extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<hui-color-picker
|
<ha-color-picker
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.defaultColor=${this.selector.ui_color?.default_color}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></hui-color-picker>
|
></ha-color-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,6 +30,8 @@ const LOAD_ELEMENTS = {
|
|||||||
entity: () => import("./ha-selector-entity"),
|
entity: () => import("./ha-selector-entity"),
|
||||||
statistic: () => import("./ha-selector-statistic"),
|
statistic: () => import("./ha-selector-statistic"),
|
||||||
file: () => import("./ha-selector-file"),
|
file: () => import("./ha-selector-file"),
|
||||||
|
floor: () => import("./ha-selector-floor"),
|
||||||
|
label: () => import("./ha-selector-label"),
|
||||||
language: () => import("./ha-selector-language"),
|
language: () => import("./ha-selector-language"),
|
||||||
navigation: () => import("./ha-selector-navigation"),
|
navigation: () => import("./ha-selector-navigation"),
|
||||||
number: () => import("./ha-selector-number"),
|
number: () => import("./ha-selector-number"),
|
||||||
|
@@ -30,6 +30,8 @@ import {
|
|||||||
entityMeetsTargetSelector,
|
entityMeetsTargetSelector,
|
||||||
expandAreaTarget,
|
expandAreaTarget,
|
||||||
expandDeviceTarget,
|
expandDeviceTarget,
|
||||||
|
expandFloorTarget,
|
||||||
|
expandLabelTarget,
|
||||||
Selector,
|
Selector,
|
||||||
} from "../data/selector";
|
} from "../data/selector";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
@@ -58,20 +60,12 @@ const showOptionalToggle = (field) =>
|
|||||||
!("boolean" in field.selector && field.default);
|
!("boolean" in field.selector && field.default);
|
||||||
|
|
||||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||||
fields: {
|
fields: Array<
|
||||||
|
Omit<HassService["fields"][string], "selector"> & {
|
||||||
key: string;
|
key: string;
|
||||||
name?: string;
|
|
||||||
description: string;
|
|
||||||
required?: boolean;
|
|
||||||
advanced?: boolean;
|
|
||||||
default?: any;
|
|
||||||
example?: any;
|
|
||||||
filter?: {
|
|
||||||
supported_features?: number[];
|
|
||||||
attribute?: Record<string, any[]>;
|
|
||||||
};
|
|
||||||
selector?: Selector;
|
selector?: Selector;
|
||||||
}[];
|
}
|
||||||
|
>;
|
||||||
hasSelector: string[];
|
hasSelector: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,10 +269,42 @@ export class HaServiceControl extends LitElement {
|
|||||||
ensureArray(
|
ensureArray(
|
||||||
value?.target?.device_id || value?.data?.device_id
|
value?.target?.device_id || value?.data?.device_id
|
||||||
)?.slice() || [];
|
)?.slice() || [];
|
||||||
const targetAreas = ensureArray(
|
const targetAreas =
|
||||||
value?.target?.area_id || value?.data?.area_id
|
ensureArray(value?.target?.area_id || value?.data?.area_id)?.slice() ||
|
||||||
|
[];
|
||||||
|
const targetFloors = ensureArray(
|
||||||
|
value?.target?.floor_id || value?.data?.floor_id
|
||||||
)?.slice();
|
)?.slice();
|
||||||
if (targetAreas) {
|
const targetLabels = ensureArray(
|
||||||
|
value?.target?.label_id || value?.data?.label_id
|
||||||
|
)?.slice();
|
||||||
|
if (targetLabels) {
|
||||||
|
targetLabels.forEach((labelId) => {
|
||||||
|
const expanded = expandLabelTarget(
|
||||||
|
this.hass,
|
||||||
|
labelId,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
targetSelector
|
||||||
|
);
|
||||||
|
targetDevices.push(...expanded.devices);
|
||||||
|
targetEntities.push(...expanded.entities);
|
||||||
|
targetAreas.push(...expanded.areas);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetFloors) {
|
||||||
|
targetFloors.forEach((floorId) => {
|
||||||
|
const expanded = expandFloorTarget(
|
||||||
|
this.hass,
|
||||||
|
floorId,
|
||||||
|
this.hass.areas,
|
||||||
|
targetSelector
|
||||||
|
);
|
||||||
|
targetAreas.push(...expanded.areas);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (targetAreas.length) {
|
||||||
targetAreas.forEach((areaId) => {
|
targetAreas.forEach((areaId) => {
|
||||||
const expanded = expandAreaTarget(
|
const expanded = expandAreaTarget(
|
||||||
this.hass,
|
this.hass,
|
||||||
|
@@ -83,7 +83,7 @@ export class HaSortable extends LitElement {
|
|||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this._shouldBeDestroy = false;
|
this._shouldBeDestroy = false;
|
||||||
if (this.hasUpdated) {
|
if (this.hasUpdated) {
|
||||||
this.requestUpdate();
|
this._createSortable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
38
src/components/ha-sub-menu.ts
Normal file
38
src/components/ha-sub-menu.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import "element-internals-polyfill";
|
||||||
|
import { CSSResult, css } from "lit";
|
||||||
|
import { MdSubMenu } from "@material/web/menu/sub-menu";
|
||||||
|
|
||||||
|
@customElement("ha-sub-menu")
|
||||||
|
// @ts-expect-error
|
||||||
|
export class HaSubMenu extends MdSubMenu {
|
||||||
|
static override styles: CSSResult[] = [
|
||||||
|
MdSubMenu.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--ha-icon-display: block;
|
||||||
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-primary: var(--primary-text-color);
|
||||||
|
--md-sys-color-secondary: var(--secondary-text-color);
|
||||||
|
--md-sys-color-surface: var(--card-background-color);
|
||||||
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||||
|
--md-sys-color-secondary-container: rgba(
|
||||||
|
var(--rgb-primary-color),
|
||||||
|
0.15
|
||||||
|
);
|
||||||
|
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||||
|
--mdc-icon-size: 16px;
|
||||||
|
|
||||||
|
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sub-menu": HaSubMenu;
|
||||||
|
}
|
||||||
|
}
|
@@ -6,38 +6,57 @@ import "@material/mwc-menu/mwc-menu-surface";
|
|||||||
import {
|
import {
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
|
mdiHome,
|
||||||
|
mdiLabel,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiSofa,
|
mdiTextureBox,
|
||||||
mdiUnfoldMoreVertical,
|
mdiUnfoldMoreVertical,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||||
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
|
import {
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing, unsafeCSS } from "lit";
|
HassEntity,
|
||||||
|
HassServiceTarget,
|
||||||
|
UnsubscribeFunc,
|
||||||
|
} from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ensureArray } from "../common/array/ensure-array";
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
|
import { hex2rgb } from "../common/color/convert-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||||
|
import { AreaRegistryEntry } from "../data/area_registry";
|
||||||
import {
|
import {
|
||||||
computeDeviceName,
|
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
|
computeDeviceName,
|
||||||
} from "../data/device_registry";
|
} from "../data/device_registry";
|
||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
|
import {
|
||||||
|
FloorRegistryEntry,
|
||||||
|
subscribeFloorRegistry,
|
||||||
|
} from "../data/floor_registry";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./device/ha-device-picker";
|
import "./device/ha-device-picker";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./entity/ha-entity-picker";
|
import "./entity/ha-entity-picker";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||||
import "./ha-area-picker";
|
import "./ha-area-floor-picker";
|
||||||
|
import { floorDefaultIconPath } from "./ha-floor-icon";
|
||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-input-helper-text";
|
import "./ha-input-helper-text";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
@customElement("ha-target-picker")
|
@customElement("ha-target-picker")
|
||||||
export class HaTargetPicker extends LitElement {
|
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: HassServiceTarget;
|
@property({ attribute: false }) public value?: HassServiceTarget;
|
||||||
@@ -72,14 +91,33 @@ export class HaTargetPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public addOnTop = false;
|
@property({ type: Boolean }) public addOnTop = false;
|
||||||
|
|
||||||
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
|
@state() private _addMode?:
|
||||||
|
| "area_id"
|
||||||
|
| "entity_id"
|
||||||
|
| "device_id"
|
||||||
|
| "label_id";
|
||||||
|
|
||||||
@query("#input") private _inputElement?;
|
@query("#input") private _inputElement?;
|
||||||
|
|
||||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||||
|
|
||||||
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
private _opened = false;
|
private _opened = false;
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
|
this._floors = floors;
|
||||||
|
}),
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (this.addOnTop) {
|
if (this.addOnTop) {
|
||||||
return html` ${this._renderChips()} ${this._renderItems()} `;
|
return html` ${this._renderChips()} ${this._renderItems()} `;
|
||||||
@@ -90,6 +128,21 @@ export class HaTargetPicker extends LitElement {
|
|||||||
private _renderItems() {
|
private _renderItems() {
|
||||||
return html`
|
return html`
|
||||||
<div class="mdc-chip-set items">
|
<div class="mdc-chip-set items">
|
||||||
|
${this.value?.floor_id
|
||||||
|
? ensureArray(this.value.floor_id).map((floor_id) => {
|
||||||
|
const floor = this._floors?.find(
|
||||||
|
(flr) => flr.floor_id === floor_id
|
||||||
|
);
|
||||||
|
return this._renderChip(
|
||||||
|
"floor_id",
|
||||||
|
floor_id,
|
||||||
|
floor?.name || floor_id,
|
||||||
|
undefined,
|
||||||
|
floor?.icon,
|
||||||
|
floor ? floorDefaultIconPath(floor) : mdiHome
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
${this.value?.area_id
|
${this.value?.area_id
|
||||||
? ensureArray(this.value.area_id).map((area_id) => {
|
? ensureArray(this.value.area_id).map((area_id) => {
|
||||||
const area = this.hass.areas![area_id];
|
const area = this.hass.areas![area_id];
|
||||||
@@ -99,10 +152,10 @@ export class HaTargetPicker extends LitElement {
|
|||||||
area?.name || area_id,
|
area?.name || area_id,
|
||||||
undefined,
|
undefined,
|
||||||
area?.icon,
|
area?.icon,
|
||||||
mdiSofa
|
mdiTextureBox
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
${this.value?.device_id
|
${this.value?.device_id
|
||||||
? ensureArray(this.value.device_id).map((device_id) => {
|
? ensureArray(this.value.device_id).map((device_id) => {
|
||||||
const device = this.hass.devices![device_id];
|
const device = this.hass.devices![device_id];
|
||||||
@@ -115,7 +168,7 @@ export class HaTargetPicker extends LitElement {
|
|||||||
mdiDevices
|
mdiDevices
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
${this.value?.entity_id
|
${this.value?.entity_id
|
||||||
? ensureArray(this.value.entity_id).map((entity_id) => {
|
? ensureArray(this.value.entity_id).map((entity_id) => {
|
||||||
const entity = this.hass.states[entity_id];
|
const entity = this.hass.states[entity_id];
|
||||||
@@ -126,7 +179,35 @@ export class HaTargetPicker extends LitElement {
|
|||||||
entity
|
entity
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: ""}
|
: nothing}
|
||||||
|
${this.value?.label_id
|
||||||
|
? ensureArray(this.value.label_id).map((label_id) => {
|
||||||
|
const label = this._labels?.find(
|
||||||
|
(lbl) => lbl.label_id === label_id
|
||||||
|
);
|
||||||
|
let color = label?.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
if (color?.startsWith("var(")) {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
color = computedStyles.getPropertyValue(
|
||||||
|
color.substring(4, color.length - 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (color?.startsWith("#")) {
|
||||||
|
color = hex2rgb(color).join(",");
|
||||||
|
}
|
||||||
|
return this._renderChip(
|
||||||
|
"label_id",
|
||||||
|
label_id,
|
||||||
|
label ? label.name : label_id,
|
||||||
|
undefined,
|
||||||
|
label?.icon,
|
||||||
|
mdiLabel,
|
||||||
|
color
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -194,6 +275,26 @@ export class HaTargetPicker extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="mdc-chip label_id add"
|
||||||
|
.type=${"label_id"}
|
||||||
|
@click=${this._showPicker}
|
||||||
|
>
|
||||||
|
<div class="mdc-chip__ripple"></div>
|
||||||
|
<ha-svg-icon
|
||||||
|
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||||
|
.path=${mdiPlus}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<span role="gridcell">
|
||||||
|
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||||
|
<span class="mdc-chip__text"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.components.target-picker.add_label_id"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
${this._renderPicker()}
|
${this._renderPicker()}
|
||||||
</div>
|
</div>
|
||||||
${this.helper
|
${this.helper
|
||||||
@@ -207,18 +308,22 @@ export class HaTargetPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderChip(
|
private _renderChip(
|
||||||
type: "area_id" | "device_id" | "entity_id",
|
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
entityState?: HassEntity,
|
entityState?: HassEntity,
|
||||||
icon?: string | null,
|
icon?: string | null,
|
||||||
fallbackIconPath?: string
|
fallbackIconPath?: string,
|
||||||
|
color?: string
|
||||||
) {
|
) {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="mdc-chip ${classMap({
|
class="mdc-chip ${classMap({
|
||||||
[type]: true,
|
[type]: true,
|
||||||
})}"
|
})}"
|
||||||
|
style=${color
|
||||||
|
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
|
||||||
|
: ""}
|
||||||
>
|
>
|
||||||
${icon
|
${icon
|
||||||
? html`<ha-icon
|
? html`<ha-icon
|
||||||
@@ -296,7 +401,7 @@ export class HaTargetPicker extends LitElement {
|
|||||||
@input=${stopPropagation}
|
@input=${stopPropagation}
|
||||||
>${this._addMode === "area_id"
|
>${this._addMode === "area_id"
|
||||||
? html`
|
? html`
|
||||||
<ha-area-picker
|
<ha-area-floor-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
id="input"
|
id="input"
|
||||||
.type=${"area_id"}
|
.type=${"area_id"}
|
||||||
@@ -309,9 +414,10 @@ export class HaTargetPicker extends LitElement {
|
|||||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeAreas=${ensureArray(this.value?.area_id)}
|
.excludeAreas=${ensureArray(this.value?.area_id)}
|
||||||
|
.excludeFloors=${ensureArray(this.value?.floor_id)}
|
||||||
@value-changed=${this._targetPicked}
|
@value-changed=${this._targetPicked}
|
||||||
@click=${this._preventDefault}
|
@click=${this._preventDefault}
|
||||||
></ha-area-picker>
|
></ha-area-floor-picker>
|
||||||
`
|
`
|
||||||
: this._addMode === "device_id"
|
: this._addMode === "device_id"
|
||||||
? html`
|
? html`
|
||||||
@@ -331,6 +437,25 @@ export class HaTargetPicker extends LitElement {
|
|||||||
@click=${this._preventDefault}
|
@click=${this._preventDefault}
|
||||||
></ha-device-picker>
|
></ha-device-picker>
|
||||||
`
|
`
|
||||||
|
: this._addMode === "label_id"
|
||||||
|
? html`
|
||||||
|
<ha-label-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
id="input"
|
||||||
|
.type=${"label_id"}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.add_label_id"
|
||||||
|
)}
|
||||||
|
no-add
|
||||||
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
.entityFilter=${this.entityFilter}
|
||||||
|
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||||
|
.includeDomains=${this.includeDomains}
|
||||||
|
.excludeLabels=${ensureArray(this.value?.label_id)}
|
||||||
|
@value-changed=${this._targetPicked}
|
||||||
|
@click=${this._preventDefault}
|
||||||
|
></ha-label-picker>
|
||||||
|
`
|
||||||
: html`
|
: html`
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -356,18 +481,24 @@ export class HaTargetPicker extends LitElement {
|
|||||||
if (!ev.detail.value) {
|
if (!ev.detail.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const value = ev.detail.value;
|
let value = ev.detail.value;
|
||||||
const target = ev.currentTarget;
|
const target = ev.currentTarget;
|
||||||
|
let type = target.type;
|
||||||
|
|
||||||
if (target.type === "entity_id" && !isValidEntityId(value)) {
|
if (type === "entity_id" && !isValidEntityId(value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "area_id") {
|
||||||
|
value = ev.detail.value.id;
|
||||||
|
type = `${ev.detail.value.type}_id`;
|
||||||
|
}
|
||||||
|
|
||||||
target.value = "";
|
target.value = "";
|
||||||
if (
|
if (
|
||||||
this.value &&
|
this.value &&
|
||||||
this.value[target.type] &&
|
this.value[type] &&
|
||||||
ensureArray(this.value[target.type]).includes(value)
|
ensureArray(this.value[type]).includes(value)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -375,19 +506,31 @@ export class HaTargetPicker extends LitElement {
|
|||||||
value: this.value
|
value: this.value
|
||||||
? {
|
? {
|
||||||
...this.value,
|
...this.value,
|
||||||
[target.type]: this.value[target.type]
|
[type]: this.value[type]
|
||||||
? [...ensureArray(this.value[target.type]), value]
|
? [...ensureArray(this.value[type]), value]
|
||||||
: value,
|
: value,
|
||||||
}
|
}
|
||||||
: { [target.type]: value },
|
: { [type]: value },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleExpand(ev) {
|
private _handleExpand(ev) {
|
||||||
const target = ev.currentTarget as any;
|
const target = ev.currentTarget as any;
|
||||||
|
const newAreas: string[] = [];
|
||||||
const newDevices: string[] = [];
|
const newDevices: string[] = [];
|
||||||
const newEntities: string[] = [];
|
const newEntities: string[] = [];
|
||||||
if (target.type === "area_id") {
|
|
||||||
|
if (target.type === "floor_id") {
|
||||||
|
Object.values(this.hass.areas).forEach((area) => {
|
||||||
|
if (
|
||||||
|
area.floor_id === target.id &&
|
||||||
|
!this.value!.area_id?.includes(area.area_id) &&
|
||||||
|
this._areaMeetsFilter(area)
|
||||||
|
) {
|
||||||
|
newAreas.push(area.area_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (target.type === "area_id") {
|
||||||
Object.values(this.hass.devices).forEach((device) => {
|
Object.values(this.hass.devices).forEach((device) => {
|
||||||
if (
|
if (
|
||||||
device.area_id === target.id &&
|
device.area_id === target.id &&
|
||||||
@@ -416,6 +559,34 @@ export class HaTargetPicker extends LitElement {
|
|||||||
newEntities.push(entity.entity_id);
|
newEntities.push(entity.entity_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (target.type === "label_id") {
|
||||||
|
Object.values(this.hass.areas).forEach((area) => {
|
||||||
|
if (
|
||||||
|
area.labels.includes(target.id) &&
|
||||||
|
!this.value!.area_id?.includes(area.area_id) &&
|
||||||
|
this._areaMeetsFilter(area)
|
||||||
|
) {
|
||||||
|
newAreas.push(area.area_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.values(this.hass.devices).forEach((device) => {
|
||||||
|
if (
|
||||||
|
device.labels.includes(target.id) &&
|
||||||
|
!this.value!.device_id?.includes(device.id) &&
|
||||||
|
this._deviceMeetsFilter(device)
|
||||||
|
) {
|
||||||
|
newDevices.push(device.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.values(this.hass.entities).forEach((entity) => {
|
||||||
|
if (
|
||||||
|
entity.labels.includes(target.id) &&
|
||||||
|
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||||
|
this._entityRegMeetsFilter(entity)
|
||||||
|
) {
|
||||||
|
newEntities.push(entity.entity_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -426,6 +597,9 @@ export class HaTargetPicker extends LitElement {
|
|||||||
if (newDevices.length) {
|
if (newDevices.length) {
|
||||||
value = this._addItems(value, "device_id", newDevices);
|
value = this._addItems(value, "device_id", newDevices);
|
||||||
}
|
}
|
||||||
|
if (newAreas.length) {
|
||||||
|
value = this._addItems(value, "area_id", newAreas);
|
||||||
|
}
|
||||||
value = this._removeItem(value, target.type, target.id);
|
value = this._removeItem(value, target.type, target.id);
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
}
|
}
|
||||||
@@ -495,45 +669,34 @@ export class HaTargetPicker extends LitElement {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _areaMeetsFilter(area: AreaRegistryEntry): boolean {
|
||||||
|
const areaDevices = Object.values(this.hass.devices).filter(
|
||||||
|
(device) => device.area_id === area.area_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (areaDevices.some((device) => this._deviceMeetsFilter(device))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaEntities = Object.values(this.hass.entities).filter(
|
||||||
|
(entity) => entity.area_id === area.area_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (areaEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
|
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
|
||||||
const devEntities = Object.values(this.hass.entities).filter(
|
const devEntities = Object.values(this.hass.entities).filter(
|
||||||
(entity) => entity.device_id === device.id
|
(entity) => entity.device_id === device.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.includeDomains) {
|
if (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||||
if (!devEntities || !devEntities.length) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
!devEntities.some((entity) =>
|
|
||||||
this.includeDomains!.includes(computeDomain(entity.entity_id))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.includeDeviceClasses) {
|
|
||||||
if (!devEntities || !devEntities.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!devEntities.some((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
stateObj.attributes.device_class &&
|
|
||||||
this.includeDeviceClasses!.includes(
|
|
||||||
stateObj.attributes.device_class
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.deviceFilter) {
|
if (this.deviceFilter) {
|
||||||
if (!this.deviceFilter(device)) {
|
if (!this.deviceFilter(device)) {
|
||||||
@@ -541,19 +704,6 @@ export class HaTargetPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.entityFilter) {
|
|
||||||
if (
|
|
||||||
!devEntities.some((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
if (!stateObj) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.entityFilter!(stateObj);
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,8 +791,8 @@ export class HaTargetPicker extends LitElement {
|
|||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-left: -14px !important;
|
margin-left: -13px !important;
|
||||||
margin-inline-start: -14px !important;
|
margin-inline-start: -13px !important;
|
||||||
margin-inline-end: 4px !important;
|
margin-inline-end: 4px !important;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
@@ -651,16 +801,19 @@ export class HaTargetPicker extends LitElement {
|
|||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
}
|
}
|
||||||
.mdc-chip.area_id:not(.add) {
|
.mdc-chip.area_id:not(.add),
|
||||||
border: 2px solid #fed6a4;
|
.mdc-chip.floor_id:not(.add) {
|
||||||
|
border: 1px solid #fed6a4;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||||
.mdc-chip.area_id.add {
|
.mdc-chip.area_id.add,
|
||||||
|
.mdc-chip.floor_id:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.floor_id.add {
|
||||||
background: #fed6a4;
|
background: #fed6a4;
|
||||||
}
|
}
|
||||||
.mdc-chip.device_id:not(.add) {
|
.mdc-chip.device_id:not(.add) {
|
||||||
border: 2px solid #a8e1fb;
|
border: 1px solid #a8e1fb;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||||
@@ -668,13 +821,21 @@ export class HaTargetPicker extends LitElement {
|
|||||||
background: #a8e1fb;
|
background: #a8e1fb;
|
||||||
}
|
}
|
||||||
.mdc-chip.entity_id:not(.add) {
|
.mdc-chip.entity_id:not(.add) {
|
||||||
border: 2px solid #d2e7b9;
|
border: 1px solid #d2e7b9;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
}
|
}
|
||||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||||
.mdc-chip.entity_id.add {
|
.mdc-chip.entity_id.add {
|
||||||
background: #d2e7b9;
|
background: #d2e7b9;
|
||||||
}
|
}
|
||||||
|
.mdc-chip.label_id:not(.add) {
|
||||||
|
border: 1px solid var(--color, #e0e0e0);
|
||||||
|
background: var(--card-background-color);
|
||||||
|
}
|
||||||
|
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
|
||||||
|
.mdc-chip.label_id.add {
|
||||||
|
background: var(--background-color, #e0e0e0);
|
||||||
|
}
|
||||||
.mdc-chip:hover {
|
.mdc-chip:hover {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
@@ -690,7 +851,7 @@ export class HaTargetPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-entity-picker,
|
ha-entity-picker,
|
||||||
ha-device-picker,
|
ha-device-picker,
|
||||||
ha-area-picker {
|
ha-area-floor-picker {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@@ -1,35 +1,8 @@
|
|||||||
import "@polymer/paper-toast/paper-toast";
|
|
||||||
import type { PaperToastElement } from "@polymer/paper-toast/paper-toast";
|
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { Constructor } from "../types";
|
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
|
||||||
|
|
||||||
const PaperToast = customElements.get(
|
|
||||||
"paper-toast"
|
|
||||||
) as Constructor<PaperToastElement>;
|
|
||||||
|
|
||||||
@customElement("ha-toast")
|
@customElement("ha-toast")
|
||||||
export class HaToast extends PaperToast {
|
export class HaToast extends Snackbar {}
|
||||||
private _resizeListener?: (obj: { matches: boolean }) => unknown;
|
|
||||||
|
|
||||||
private _mediaq?: MediaQueryList;
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
if (!this._resizeListener) {
|
|
||||||
this._resizeListener = (ev) =>
|
|
||||||
this.classList.toggle("fit-bottom", ev.matches);
|
|
||||||
this._mediaq = window.matchMedia("(max-width: 599px");
|
|
||||||
}
|
|
||||||
this._mediaq!.addListener(this._resizeListener);
|
|
||||||
this._resizeListener(this._mediaq!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._mediaq!.removeListener(this._resizeListener!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
|
36
src/components/ha-tree-indicator.ts
Normal file
36
src/components/ha-tree-indicator.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
|
@customElement("ha-tree-indicator")
|
||||||
|
export class HaTreeIndicator extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true })
|
||||||
|
public end?: boolean = false;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 48 48">
|
||||||
|
<line x1="24" y1="0" x2="24" y2=${this.end ? "24" : "48"}></line>
|
||||||
|
<line x1="24" y1="24" x2="36" y2="24"></line>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
line {
|
||||||
|
stroke: var(--divider-color);
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: 2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-tree-indicator": HaTreeIndicator;
|
||||||
|
}
|
||||||
|
}
|
118
src/components/search-input-outlined.ts
Normal file
118
src/components/search-input-outlined.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-outlined-text-field";
|
||||||
|
import type { HaOutlinedTextField } from "./ha-outlined-text-field";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
@customElement("search-input-outlined")
|
||||||
|
class SearchInputOutlined extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public filter?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public suffix = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public label?: string;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder?: string;
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
this._input?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@query("ha-outlined-text-field", true) private _input!: HaOutlinedTextField;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const placeholder =
|
||||||
|
this.placeholder || this.hass.localize("ui.common.search");
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-outlined-text-field
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.aria-label=${this.label || this.hass.localize("ui.common.search")}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${this.filter || ""}
|
||||||
|
icon
|
||||||
|
.iconTrailing=${this.filter || this.suffix}
|
||||||
|
@input=${this._filterInputChanged}
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<slot name="prefix" slot="leading-icon">
|
||||||
|
<ha-svg-icon
|
||||||
|
tabindex="-1"
|
||||||
|
class="prefix"
|
||||||
|
.path=${mdiMagnify}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</slot>
|
||||||
|
${this.filter
|
||||||
|
? html`<ha-icon-button
|
||||||
|
aria-label="Clear input"
|
||||||
|
slot="trailing-icon"
|
||||||
|
@click=${this._clearSearch}
|
||||||
|
.path=${mdiClose}
|
||||||
|
>
|
||||||
|
</ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
|
</ha-outlined-text-field>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filterChanged(value: string) {
|
||||||
|
fireEvent(this, "value-changed", { value: String(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filterInputChanged(e) {
|
||||||
|
this._filterChanged(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _clearSearch() {
|
||||||
|
this._filterChanged("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
/* For iOS */
|
||||||
|
z-index: 0;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
}
|
||||||
|
ha-outlined-text-field {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
--ha-outlined-text-field-container-color: var(--card-background-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon,
|
||||||
|
ha-icon-button {
|
||||||
|
display: flex;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"search-input-outlined": SearchInputOutlined;
|
||||||
|
}
|
||||||
|
}
|
@@ -163,12 +163,7 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
<br />
|
<br />
|
||||||
${result
|
${error
|
||||||
? html`${this.hass!.localize(
|
|
||||||
"ui.panel.config.automation.trace.path.result"
|
|
||||||
)}
|
|
||||||
<pre>${dump(result)}</pre>`
|
|
||||||
: error
|
|
||||||
? html`<div class="error">
|
? html`<div class="error">
|
||||||
${this.hass!.localize(
|
${this.hass!.localize(
|
||||||
"ui.panel.config.automation.trace.path.error",
|
"ui.panel.config.automation.trace.path.error",
|
||||||
@@ -178,6 +173,12 @@ export class HaTracePathDetails extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${result
|
||||||
|
? html`${this.hass!.localize(
|
||||||
|
"ui.panel.config.automation.trace.path.result"
|
||||||
|
)}
|
||||||
|
<pre>${dump(result)}</pre>`
|
||||||
|
: nothing}
|
||||||
${Object.keys(rest).length === 0
|
${Object.keys(rest).length === 0
|
||||||
? nothing
|
? nothing
|
||||||
: html`<pre>${dump(rest)}</pre>`}
|
: html`<pre>${dump(rest)}</pre>`}
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
|
import { mdiExclamationThick } from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
html,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
svg,
|
css,
|
||||||
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
|
svg,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { NODE_SIZE, SPACING } from "./hat-graph-const";
|
|
||||||
import { isSafari } from "../../util/is_safari";
|
import { isSafari } from "../../util/is_safari";
|
||||||
|
import { NODE_SIZE, SPACING } from "./hat-graph-const";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @attribute active
|
* @attribute active
|
||||||
@@ -21,6 +22,8 @@ export class HatGraphNode extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public error = false;
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) notEnabled = false;
|
@property({ reflect: true, type: Boolean }) notEnabled = false;
|
||||||
|
|
||||||
@property({ reflect: true, type: Boolean }) graphStart = false;
|
@property({ reflect: true, type: Boolean }) graphStart = false;
|
||||||
@@ -65,16 +68,28 @@ export class HatGraphNode extends LitElement {
|
|||||||
`}
|
`}
|
||||||
<g class="node">
|
<g class="node">
|
||||||
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
|
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
|
||||||
|
${this.error
|
||||||
|
? svg`
|
||||||
|
<g class="error">
|
||||||
|
<circle
|
||||||
|
cx="-12"
|
||||||
|
cy=${-NODE_SIZE / 2}
|
||||||
|
r="8"
|
||||||
|
></circle>
|
||||||
|
<path transform="translate(-18 -21) scale(.5)" class="exclamation" d=${mdiExclamationThick}/>
|
||||||
|
</g>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
${this.badge
|
${this.badge
|
||||||
? svg`
|
? svg`
|
||||||
<g class="number">
|
<g class="number">
|
||||||
<circle
|
<circle
|
||||||
cx="8"
|
cx="12"
|
||||||
cy=${-NODE_SIZE / 2}
|
cy=${-NODE_SIZE / 2}
|
||||||
r="8"
|
r="8"
|
||||||
></circle>
|
></circle>
|
||||||
<text
|
<text
|
||||||
x="8"
|
x="12"
|
||||||
y=${-NODE_SIZE / 2}
|
y=${-NODE_SIZE / 2}
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
alignment-baseline="middle"
|
alignment-baseline="middle"
|
||||||
@@ -82,7 +97,7 @@ export class HatGraphNode extends LitElement {
|
|||||||
</g>
|
</g>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<g style="pointer-events: none" transform="translate(${-12} ${-12})">
|
<g style="pointer-events: none" transform="translate(-12 -12)">
|
||||||
${this.iconPath
|
${this.iconPath
|
||||||
? svg`<path class="icon" d=${this.iconPath}/>`
|
? svg`<path class="icon" d=${this.iconPath}/>`
|
||||||
: svg`<foreignObject><span class="icon"><slot name="icon"></slot></span></foreignObject>`}
|
: svg`<foreignObject><span class="icon"><slot name="icon"></slot></span></foreignObject>`}
|
||||||
@@ -143,13 +158,22 @@ export class HatGraphNode extends LitElement {
|
|||||||
fill: var(--background-clr);
|
fill: var(--background-clr);
|
||||||
stroke: var(--circle-clr, var(--stroke-clr));
|
stroke: var(--circle-clr, var(--stroke-clr));
|
||||||
}
|
}
|
||||||
|
.error circle {
|
||||||
|
fill: var(--error-color);
|
||||||
|
stroke: none;
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
.error .exclamation {
|
||||||
|
fill: var(--text-primary-color);
|
||||||
|
}
|
||||||
.number circle {
|
.number circle {
|
||||||
fill: var(--track-clr);
|
fill: var(--track-clr);
|
||||||
stroke: none;
|
stroke: none;
|
||||||
stroke-width: 0;
|
stroke-width: 0;
|
||||||
}
|
}
|
||||||
.number text {
|
.number text {
|
||||||
font-size: smaller;
|
font-size: 10px;
|
||||||
|
fill: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
path.icon {
|
path.icon {
|
||||||
fill: var(--icon-clr);
|
fill: var(--icon-clr);
|
||||||
|
@@ -93,6 +93,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
.iconPath=${mdiAsterisk}
|
.iconPath=${mdiAsterisk}
|
||||||
.notEnabled=${config.enabled === false}
|
.notEnabled=${config.enabled === false}
|
||||||
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
tabindex=${track ? "0" : "-1"}
|
tabindex=${track ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@@ -171,6 +172,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
?track=${trace !== undefined}
|
?track=${trace !== undefined}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
.notEnabled=${disabled || config.enabled === false}
|
.notEnabled=${disabled || config.enabled === false}
|
||||||
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
slot="head"
|
slot="head"
|
||||||
nofocus
|
nofocus
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
@@ -424,6 +426,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
?track=${path in this.trace.trace}
|
?track=${path in this.trace.trace}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
.notEnabled=${disabled || node.enabled === false}
|
.notEnabled=${disabled || node.enabled === false}
|
||||||
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
>
|
>
|
||||||
${node.service
|
${node.service
|
||||||
@@ -451,6 +454,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
?track=${path in this.trace.trace}
|
?track=${path in this.trace.trace}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
.notEnabled=${disabled || node.enabled === false}
|
.notEnabled=${disabled || node.enabled === false}
|
||||||
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
@@ -517,6 +521,7 @@ export class HatScriptGraph extends LitElement {
|
|||||||
@focus=${this.selectNode(node, path)}
|
@focus=${this.selectNode(node, path)}
|
||||||
?track=${path in this.trace.trace}
|
?track=${path in this.trace.trace}
|
||||||
?active=${this.selected === path}
|
?active=${this.selected === path}
|
||||||
|
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||||
.notEnabled=${disabled || node.enabled === false}
|
.notEnabled=${disabled || node.enabled === false}
|
||||||
></hat-graph-node>
|
></hat-graph-node>
|
||||||
`;
|
`;
|
||||||
|
@@ -153,7 +153,7 @@ class LogbookRenderer {
|
|||||||
|
|
||||||
const parts: TemplateResult[] = [];
|
const parts: TemplateResult[] = [];
|
||||||
|
|
||||||
let i;
|
let i: number;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
i = 0;
|
i = 0;
|
||||||
@@ -232,7 +232,7 @@ class ActionRenderer {
|
|||||||
const value = this._getItem(index);
|
const value = this._getItem(index);
|
||||||
|
|
||||||
if (renderAllIterations) {
|
if (renderAllIterations) {
|
||||||
let i;
|
let i: number = 0;
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
i = this._renderIteration(index, item, actionType);
|
i = this._renderIteration(index, item, actionType);
|
||||||
});
|
});
|
||||||
@@ -270,7 +270,12 @@ class ActionRenderer {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
path,
|
path,
|
||||||
`Unable to extract path ${path}. Download trace and report as bug`
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.path_error",
|
||||||
|
{
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return index + 1;
|
return index + 1;
|
||||||
}
|
}
|
||||||
@@ -324,20 +329,22 @@ class ActionRenderer {
|
|||||||
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
|
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
triggerStep.path,
|
triggerStep.path,
|
||||||
`${
|
this.hass.localize(
|
||||||
triggerStep.changed_variables.trigger.alias
|
"ui.panel.config.automation.trace.messages.triggered_by",
|
||||||
? `${triggerStep.changed_variables.trigger.alias} triggered`
|
{
|
||||||
: "Triggered"
|
triggeredBy: triggerStep.changed_variables.trigger?.alias
|
||||||
} ${
|
? "alias"
|
||||||
triggerStep.path === "trigger"
|
: "other",
|
||||||
? "manually"
|
alias: triggerStep.changed_variables.trigger?.alias,
|
||||||
: `by the ${this.trace.trigger}`
|
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
|
||||||
} at
|
trigger: this.trace.trigger,
|
||||||
${formatDateTimeWithSeconds(
|
time: formatDateTimeWithSeconds(
|
||||||
new Date(triggerStep.timestamp),
|
new Date(triggerStep.timestamp),
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
this.hass.config
|
this.hass.config
|
||||||
)}`,
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
mdiCircle
|
mdiCircle
|
||||||
);
|
);
|
||||||
return index + 1;
|
return index + 1;
|
||||||
@@ -367,12 +374,17 @@ class ActionRenderer {
|
|||||||
this.keys[index]
|
this.keys[index]
|
||||||
) as ChooseAction;
|
) as ChooseAction;
|
||||||
const disabled = chooseConfig.enabled === false;
|
const disabled = chooseConfig.enabled === false;
|
||||||
const name = chooseConfig.alias || "Choose";
|
const name =
|
||||||
|
chooseConfig.alias ||
|
||||||
|
this.hass.localize("ui.panel.config.automation.trace.messages.choose");
|
||||||
|
|
||||||
if (defaultExecuted) {
|
if (defaultExecuted) {
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
choosePath,
|
choosePath,
|
||||||
`${name}: Default action executed`,
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.default_action_executed",
|
||||||
|
{ name: name }
|
||||||
|
),
|
||||||
undefined,
|
undefined,
|
||||||
disabled
|
disabled
|
||||||
);
|
);
|
||||||
@@ -385,8 +397,17 @@ class ActionRenderer {
|
|||||||
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
|
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
|
||||||
) as ChooseActionChoice | undefined;
|
) as ChooseActionChoice | undefined;
|
||||||
const choiceName = choiceConfig
|
const choiceName = choiceConfig
|
||||||
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
|
? `${
|
||||||
: `Error: ${chooseTrace.error}`;
|
choiceConfig.alias ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.option_executed",
|
||||||
|
{ option: choiceNumeric }
|
||||||
|
)
|
||||||
|
}`
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.error",
|
||||||
|
{ error: chooseTrace.error }
|
||||||
|
);
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
choosePath,
|
choosePath,
|
||||||
`${name}: ${choiceName}`,
|
`${name}: ${choiceName}`,
|
||||||
@@ -396,13 +417,16 @@ class ActionRenderer {
|
|||||||
} else {
|
} else {
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
choosePath,
|
choosePath,
|
||||||
`${name}: No action taken`,
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.no_action_executed",
|
||||||
|
{ name: name }
|
||||||
|
),
|
||||||
undefined,
|
undefined,
|
||||||
disabled
|
disabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let i;
|
let i: number;
|
||||||
|
|
||||||
// Skip over conditions
|
// Skip over conditions
|
||||||
for (i = index + 1; i < this.keys.length; i++) {
|
for (i = index + 1; i < this.keys.length; i++) {
|
||||||
@@ -479,26 +503,38 @@ class ActionRenderer {
|
|||||||
const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
|
const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
|
||||||
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
|
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
|
||||||
const disabled = ifConfig.enabled === false;
|
const disabled = ifConfig.enabled === false;
|
||||||
const name = ifConfig.alias || "If";
|
const name =
|
||||||
|
ifConfig.alias ||
|
||||||
|
this.hass.localize("ui.panel.config.automation.trace.messages.if");
|
||||||
|
|
||||||
if (ifTrace.result?.choice) {
|
if (ifTrace.result?.choice) {
|
||||||
const choiceConfig = this._getDataFromPath(
|
const choiceConfig = this._getDataFromPath(
|
||||||
`${this.keys[index]}/${ifTrace.result.choice}/`
|
`${this.keys[index]}/${ifTrace.result.choice}/`
|
||||||
) as any;
|
) as any;
|
||||||
const choiceName = choiceConfig
|
const choiceName = choiceConfig
|
||||||
? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}`
|
? choiceConfig.alias ||
|
||||||
: `Error: ${ifTrace.error}`;
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.action_executed",
|
||||||
|
{ action: ifTrace.result.choice }
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.error",
|
||||||
|
{ error: ifTrace.error }
|
||||||
|
);
|
||||||
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled);
|
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled);
|
||||||
} else {
|
} else {
|
||||||
this._renderEntry(
|
this._renderEntry(
|
||||||
ifPath,
|
ifPath,
|
||||||
`${name}: No action taken`,
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.no_action_executed",
|
||||||
|
{ name: name }
|
||||||
|
),
|
||||||
undefined,
|
undefined,
|
||||||
disabled
|
disabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let i;
|
let i: number;
|
||||||
|
|
||||||
// Skip over conditions
|
// Skip over conditions
|
||||||
for (i = index + 1; i < this.keys.length; i++) {
|
for (i = index + 1; i < this.keys.length; i++) {
|
||||||
@@ -534,7 +570,11 @@ class ActionRenderer {
|
|||||||
|
|
||||||
const disabled = parallelConfig.enabled === false;
|
const disabled = parallelConfig.enabled === false;
|
||||||
|
|
||||||
const name = parallelConfig.alias || "Execute in parallel";
|
const name =
|
||||||
|
parallelConfig.alias ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.execute_in_parallel"
|
||||||
|
);
|
||||||
|
|
||||||
this._renderEntry(parallelPath, name, undefined, disabled);
|
this._renderEntry(parallelPath, name, undefined, disabled);
|
||||||
|
|
||||||
@@ -564,7 +604,11 @@ class ActionRenderer {
|
|||||||
this.entries.push(html`
|
this.entries.push(html`
|
||||||
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
|
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
|
||||||
${description}${disabled
|
${description}${disabled
|
||||||
? html`<span class="disabled"> (disabled)</span>`
|
? html`<span class="disabled">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.disabled"
|
||||||
|
)}</span
|
||||||
|
>`
|
||||||
: ""}
|
: ""}
|
||||||
</ha-timeline>
|
</ha-timeline>
|
||||||
`);
|
`);
|
||||||
@@ -636,13 +680,12 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
this.hass.config
|
this.hass.config
|
||||||
);
|
);
|
||||||
const renderRuntime = () => `(runtime:
|
const renderRuntime = () =>
|
||||||
${(
|
(
|
||||||
(new Date(this.trace!.timestamp.finish!).getTime() -
|
(new Date(this.trace!.timestamp.finish!).getTime() -
|
||||||
new Date(this.trace!.timestamp.start).getTime()) /
|
new Date(this.trace!.timestamp.start).getTime()) /
|
||||||
1000
|
1000
|
||||||
).toFixed(2)}
|
).toFixed(2);
|
||||||
seconds)`;
|
|
||||||
|
|
||||||
let entry: {
|
let entry: {
|
||||||
description: TemplateResult | string;
|
description: TemplateResult | string;
|
||||||
@@ -652,57 +695,90 @@ export class HaAutomationTracer extends LitElement {
|
|||||||
|
|
||||||
if (this.trace.state === "running") {
|
if (this.trace.state === "running") {
|
||||||
entry = {
|
entry = {
|
||||||
description: "Still running",
|
description: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.still_running"
|
||||||
|
),
|
||||||
icon: mdiProgressClock,
|
icon: mdiProgressClock,
|
||||||
};
|
};
|
||||||
} else if (this.trace.state === "debugged") {
|
} else if (this.trace.state === "debugged") {
|
||||||
entry = {
|
entry = {
|
||||||
description: "Debugged",
|
description: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.debugged"
|
||||||
|
),
|
||||||
icon: mdiProgressWrench,
|
icon: mdiProgressWrench,
|
||||||
};
|
};
|
||||||
} else if (this.trace.script_execution === "finished") {
|
} else if (this.trace.script_execution === "finished") {
|
||||||
entry = {
|
entry = {
|
||||||
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
|
description: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.finished",
|
||||||
|
{
|
||||||
|
time: renderFinishedAt(),
|
||||||
|
executiontime: renderRuntime(),
|
||||||
|
}
|
||||||
|
),
|
||||||
icon: mdiCircle,
|
icon: mdiCircle,
|
||||||
};
|
};
|
||||||
} else if (this.trace.script_execution === "aborted") {
|
} else if (this.trace.script_execution === "aborted") {
|
||||||
entry = {
|
entry = {
|
||||||
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
|
description: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.aborted",
|
||||||
|
{
|
||||||
|
time: renderFinishedAt(),
|
||||||
|
executiontime: renderRuntime(),
|
||||||
|
}
|
||||||
|
),
|
||||||
icon: mdiAlertCircle,
|
icon: mdiAlertCircle,
|
||||||
};
|
};
|
||||||
} else if (this.trace.script_execution === "cancelled") {
|
} else if (this.trace.script_execution === "cancelled") {
|
||||||
entry = {
|
entry = {
|
||||||
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
|
description: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.trace.messages.cancelled",
|
||||||
|
{
|
||||||
|
time: renderFinishedAt(),
|
||||||
|
executiontime: renderRuntime(),
|
||||||
|
}
|
||||||
|
),
|
||||||
icon: mdiAlertCircle,
|
icon: mdiAlertCircle,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let reason: string;
|
let message:
|
||||||
|
| "stopped_failed_conditions"
|
||||||
|
| "stopped_failed_single"
|
||||||
|
| "stopped_failed_max_runs"
|
||||||
|
| "stopped_error"
|
||||||
|
| "stopped_unknown_reason";
|
||||||
let isError = false;
|
let isError = false;
|
||||||
let extra: TemplateResult | undefined;
|
let extra: TemplateResult | undefined;
|
||||||
|
|
||||||
switch (this.trace.script_execution) {
|
switch (this.trace.script_execution) {
|
||||||
case "failed_conditions":
|
case "failed_conditions":
|
||||||
reason = "a condition failed";
|
message = "stopped_failed_conditions";
|
||||||
break;
|
break;
|
||||||
case "failed_single":
|
case "failed_single":
|
||||||
reason = "only a single execution is allowed";
|
message = "stopped_failed_single";
|
||||||
break;
|
break;
|
||||||
case "failed_max_runs":
|
case "failed_max_runs":
|
||||||
reason = "maximum number of parallel runs reached";
|
message = "stopped_failed_max_runs";
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
reason = "an error was encountered";
|
|
||||||
isError = true;
|
isError = true;
|
||||||
|
message = "stopped_error";
|
||||||
extra = html`<br /><br />${this.trace.error!}`;
|
extra = html`<br /><br />${this.trace.error!}`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
reason = `of unknown reason "${this.trace.script_execution}"`;
|
|
||||||
isError = true;
|
isError = true;
|
||||||
|
message = "stopped_unknown_reason";
|
||||||
}
|
}
|
||||||
|
|
||||||
entry = {
|
entry = {
|
||||||
description: html`Stopped because ${reason} at ${renderFinishedAt()}
|
description: html`${this.hass.localize(
|
||||||
${renderRuntime()}${extra || ""}`,
|
`ui.panel.config.automation.trace.messages.${message}`,
|
||||||
|
{
|
||||||
|
time: renderFinishedAt(),
|
||||||
|
executiontime: renderRuntime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
${extra || ""}`,
|
||||||
icon: mdiAlertCircle,
|
icon: mdiAlertCircle,
|
||||||
className: isError ? "error" : undefined,
|
className: isError ? "error" : undefined,
|
||||||
};
|
};
|
||||||
|
@@ -7,9 +7,11 @@ export { subscribeAreaRegistry } from "./ws-area_registry";
|
|||||||
|
|
||||||
export interface AreaRegistryEntry {
|
export interface AreaRegistryEntry {
|
||||||
area_id: string;
|
area_id: string;
|
||||||
|
floor_id: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
picture: string | null;
|
picture: string | null;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
|
labels: string[];
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,9 +25,11 @@ export interface AreaDeviceLookup {
|
|||||||
|
|
||||||
export interface AreaRegistryEntryMutableParams {
|
export interface AreaRegistryEntryMutableParams {
|
||||||
name: string;
|
name: string;
|
||||||
|
floor_id?: string | null;
|
||||||
picture?: string | null;
|
picture?: string | null;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
|
labels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createAreaRegistryEntry = (
|
export const createAreaRegistryEntry = (
|
||||||
|
@@ -219,8 +219,8 @@ export interface NumericStateCondition extends BaseCondition {
|
|||||||
condition: "numeric_state";
|
condition: "numeric_state";
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
above?: number;
|
above?: string | number;
|
||||||
below?: number;
|
below?: string | number;
|
||||||
value_template?: string;
|
value_template?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user