mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-18 01:19:49 +00:00
Compare commits
309 Commits
20221201.1
...
int2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d154872955 | ||
![]() |
555f0dbc81 | ||
![]() |
076c58acea | ||
![]() |
c873ea0bf8 | ||
![]() |
2b2dd74672 | ||
![]() |
73b49642f8 | ||
![]() |
cfdbad504c | ||
![]() |
26730cb2e2 | ||
![]() |
a77589922c | ||
![]() |
545f81fa24 | ||
![]() |
265ca40df1 | ||
![]() |
58cb775627 | ||
![]() |
815d4c165d | ||
![]() |
945d992884 | ||
![]() |
574396f369 | ||
![]() |
42d3adad26 | ||
![]() |
7923c172ff | ||
![]() |
fd22afedd0 | ||
![]() |
ced00a4945 | ||
![]() |
9c2a72d240 | ||
![]() |
89c2db1f8e | ||
![]() |
6ba31da4a5 | ||
![]() |
1c516822a5 | ||
![]() |
be741feb2a | ||
![]() |
72605051e4 | ||
![]() |
9f9ab6c238 | ||
![]() |
1e4a3c398b | ||
![]() |
690e2a3aa9 | ||
![]() |
e1d17f3d3a | ||
![]() |
45316b458f | ||
![]() |
51a69f1042 | ||
![]() |
e4ebb320e5 | ||
![]() |
e9c5e51f25 | ||
![]() |
4091205b58 | ||
![]() |
478539d58d | ||
![]() |
522c7c08a9 | ||
![]() |
dc140f132d | ||
![]() |
4c76a2549b | ||
![]() |
a5c77928d2 | ||
![]() |
d57cf65edc | ||
![]() |
21f58356bd | ||
![]() |
79cb7bda04 | ||
![]() |
ec844560af | ||
![]() |
d3e20429a4 | ||
![]() |
2f1d783d22 | ||
![]() |
558f523207 | ||
![]() |
c7f4693f0a | ||
![]() |
282823ee83 | ||
![]() |
014ae06e85 | ||
![]() |
9e5442db18 | ||
![]() |
789a69fea5 | ||
![]() |
c7da1a95da | ||
![]() |
6d527d5ebe | ||
![]() |
9d529d8f13 | ||
![]() |
468aeda74a | ||
![]() |
f6a508683c | ||
![]() |
64fabbbf05 | ||
![]() |
9b95814f1b | ||
![]() |
2f2cde328c | ||
![]() |
8294e0e271 | ||
![]() |
c467ef82ea | ||
![]() |
bdcd1a0a88 | ||
![]() |
36710d7588 | ||
![]() |
f09859ab40 | ||
![]() |
e206e86fb3 | ||
![]() |
21f3eb9103 | ||
![]() |
932614e31a | ||
![]() |
c1e905e03a | ||
![]() |
87d781f786 | ||
![]() |
c34e845886 | ||
![]() |
412587a457 | ||
![]() |
5b3a13f8d4 | ||
![]() |
1ec5172370 | ||
![]() |
1d20d6979e | ||
![]() |
207380d0da | ||
![]() |
d2c091536e | ||
![]() |
2eb5335a68 | ||
![]() |
1aa23d75b0 | ||
![]() |
663b6e0682 | ||
![]() |
c7f3293c1d | ||
![]() |
c63b64cff5 | ||
![]() |
7136142437 | ||
![]() |
af9a8fa3e7 | ||
![]() |
b74a2fd277 | ||
![]() |
e5350a4246 | ||
![]() |
9e5a59fbc4 | ||
![]() |
45e7f8ae06 | ||
![]() |
dcedafaef4 | ||
![]() |
8fac5f6d75 | ||
![]() |
5094e8f428 | ||
![]() |
74fb8b0427 | ||
![]() |
78768b1e2f | ||
![]() |
d55307098a | ||
![]() |
7286aa7dc4 | ||
![]() |
b44b22c723 | ||
![]() |
dcee89caeb | ||
![]() |
d1caeed05e | ||
![]() |
f2a67a5fa9 | ||
![]() |
b2d5304488 | ||
![]() |
e9aded77da | ||
![]() |
e1a94c679f | ||
![]() |
616bced37c | ||
![]() |
1585c6bf52 | ||
![]() |
f2fa433326 | ||
![]() |
dd109b0054 | ||
![]() |
9750e0e0b5 | ||
![]() |
b36eba0916 | ||
![]() |
18a69d633f | ||
![]() |
bf6ad3d0a5 | ||
![]() |
9836912efa | ||
![]() |
f31a7c3af0 | ||
![]() |
44d91eaa4f | ||
![]() |
3cc1cb7893 | ||
![]() |
e7354ed5a2 | ||
![]() |
e3ac2c149d | ||
![]() |
afcd45a780 | ||
![]() |
fe87466351 | ||
![]() |
bdef924426 | ||
![]() |
86ea3082f7 | ||
![]() |
0374330676 | ||
![]() |
d8a68326fb | ||
![]() |
4901d50918 | ||
![]() |
a16e41a7ac | ||
![]() |
f1d644ac51 | ||
![]() |
a9378abe31 | ||
![]() |
5c2fcd7f9b | ||
![]() |
0015559e24 | ||
![]() |
2fbe6809c1 | ||
![]() |
e926091e54 | ||
![]() |
1198f983aa | ||
![]() |
6a15216104 | ||
![]() |
b99a139f51 | ||
![]() |
0e9a013549 | ||
![]() |
1d1ff410b2 | ||
![]() |
d4d3a1cb65 | ||
![]() |
81e3652446 | ||
![]() |
adb61ab99b | ||
![]() |
1c139d0bc7 | ||
![]() |
419f23879a | ||
![]() |
e175c7ba3c | ||
![]() |
2575d35f2c | ||
![]() |
5eb45209e8 | ||
![]() |
0e70b866ae | ||
![]() |
c6aa2886ed | ||
![]() |
77e01812d1 | ||
![]() |
a5863a9a67 | ||
![]() |
526c34993c | ||
![]() |
3199319830 | ||
![]() |
6bb350b5ec | ||
![]() |
f41330a29b | ||
![]() |
7780ae8f76 | ||
![]() |
40cf15c1f3 | ||
![]() |
9be6a47d88 | ||
![]() |
5933c2eb8e | ||
![]() |
a8b7937d75 | ||
![]() |
4919341871 | ||
![]() |
36e99c3c0f | ||
![]() |
825008e24a | ||
![]() |
ae04a5457e | ||
![]() |
a7c3774c29 | ||
![]() |
9c24dbe333 | ||
![]() |
50f089fd4f | ||
![]() |
019ef4ba8f | ||
![]() |
b31a9d590e | ||
![]() |
43ea175a1a | ||
![]() |
b2f0b6a814 | ||
![]() |
614496d65c | ||
![]() |
d121c1cd18 | ||
![]() |
b18160d987 | ||
![]() |
5b17c59a56 | ||
![]() |
139cbb363c | ||
![]() |
e8e4733fc9 | ||
![]() |
b4d6fc3c20 | ||
![]() |
25a5bd568a | ||
![]() |
77b8152c55 | ||
![]() |
ebcbfda92d | ||
![]() |
01a4b55ed8 | ||
![]() |
311d11f2da | ||
![]() |
c400e771cb | ||
![]() |
7611a99f55 | ||
![]() |
1044b3c399 | ||
![]() |
239d3ca00c | ||
![]() |
00c2cb731b | ||
![]() |
ef7d839c0f | ||
![]() |
66a22ae102 | ||
![]() |
d48853fcdd | ||
![]() |
175a388822 | ||
![]() |
872395bec5 | ||
![]() |
5faf7cf0af | ||
![]() |
e768c78dce | ||
![]() |
50cc8594be | ||
![]() |
363092ff03 | ||
![]() |
1bce5efc9e | ||
![]() |
14513e5905 | ||
![]() |
b168f8d027 | ||
![]() |
e151520d74 | ||
![]() |
02b763e8f3 | ||
![]() |
498102ddd9 | ||
![]() |
6aba5c1017 | ||
![]() |
2176d4dcea | ||
![]() |
9fdef3df6d | ||
![]() |
d8b4611c24 | ||
![]() |
9c27bb37a0 | ||
![]() |
f2b7288e92 | ||
![]() |
f7aecb0d6d | ||
![]() |
1da8a974f8 | ||
![]() |
b82d6fd35f | ||
![]() |
62bc171b8c | ||
![]() |
2d7973af79 | ||
![]() |
d64bb98bb4 | ||
![]() |
5cabf1d041 | ||
![]() |
180357e0db | ||
![]() |
c1dba217da | ||
![]() |
8d1ecdb27e | ||
![]() |
f83544dd38 | ||
![]() |
2ae137bbc2 | ||
![]() |
9f8aa0b4bf | ||
![]() |
149fa5fca6 | ||
![]() |
0fb35fd0d0 | ||
![]() |
544272b4df | ||
![]() |
b939978de1 | ||
![]() |
d74fb8717c | ||
![]() |
f204a163d4 | ||
![]() |
bc9fb9a472 | ||
![]() |
1e654d9661 | ||
![]() |
03d33759b8 | ||
![]() |
ef7d696f9d | ||
![]() |
545141da92 | ||
![]() |
467957005d | ||
![]() |
348c3c9787 | ||
![]() |
fe0492c2e0 | ||
![]() |
0b377c060c | ||
![]() |
0f971e5868 | ||
![]() |
1dbe8c9b64 | ||
![]() |
6e4a6cb0db | ||
![]() |
3f1903bd87 | ||
![]() |
9643e0268f | ||
![]() |
23573d8c26 | ||
![]() |
3c8c1260b1 | ||
![]() |
b84de87688 | ||
![]() |
2d30994e56 | ||
![]() |
4ea19a6041 | ||
![]() |
af82c0d0c4 | ||
![]() |
2b5ef1207c | ||
![]() |
feddbdba96 | ||
![]() |
b581b95d9d | ||
![]() |
8c69c772d1 | ||
![]() |
ba09120ff3 | ||
![]() |
a8d242719f | ||
![]() |
0d0028fced | ||
![]() |
d1b1eecd92 | ||
![]() |
3b51050b52 | ||
![]() |
34e1d745be | ||
![]() |
efda2ab626 | ||
![]() |
ae354fa8da | ||
![]() |
ef130be352 | ||
![]() |
f1393e5f00 | ||
![]() |
dac784553e | ||
![]() |
d2ad67384f | ||
![]() |
69d16ab9b4 | ||
![]() |
a941ed4f90 | ||
![]() |
774259224b | ||
![]() |
8a5f781ca2 | ||
![]() |
dacef605c7 | ||
![]() |
2c279e2dec | ||
![]() |
06ee08db36 | ||
![]() |
34dfaa5a0f | ||
![]() |
b06db26540 | ||
![]() |
076a6c4459 | ||
![]() |
0001cad423 | ||
![]() |
d622d9d420 | ||
![]() |
228fd4b7fe | ||
![]() |
e6f2e8058b | ||
![]() |
83ba594cc4 | ||
![]() |
ff64dc2631 | ||
![]() |
745753c526 | ||
![]() |
f5385ba277 | ||
![]() |
1a69503eab | ||
![]() |
8ba46f7f57 | ||
![]() |
bbdb84482a | ||
![]() |
08ffe375b9 | ||
![]() |
8699f3e3a8 | ||
![]() |
ecdd07ff4d | ||
![]() |
f0f699a37e | ||
![]() |
75b1b1c9a0 | ||
![]() |
0ae8246d8a | ||
![]() |
b644407260 | ||
![]() |
b389127f78 | ||
![]() |
20dff9d25d | ||
![]() |
076ddb71b6 | ||
![]() |
f0127511b0 | ||
![]() |
07ad429f8c | ||
![]() |
ef3caf91f1 | ||
![]() |
4fcfcbeefb | ||
![]() |
f18997c7c3 | ||
![]() |
2ed8a4053b | ||
![]() |
1105c92569 | ||
![]() |
76a682fa28 | ||
![]() |
d059b97a2f | ||
![]() |
96080f3c78 | ||
![]() |
00274ebf66 | ||
![]() |
7e58bd59c3 | ||
![]() |
04ef783f5b | ||
![]() |
340449d064 | ||
![]() |
24f2ad8be9 | ||
![]() |
3eac53e209 | ||
![]() |
65cef9d996 | ||
![]() |
4c5f4508b2 | ||
![]() |
1aa6bd5577 |
@@ -5,33 +5,39 @@
|
||||
"context": ".."
|
||||
},
|
||||
"appPort": "8124:8123",
|
||||
"context": "..",
|
||||
"postCreateCommand": "script/bootstrap",
|
||||
"extensions": [
|
||||
"github.vscode-pull-request-github",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"runem.lit-plugin",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
"containerEnv": {
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"runem.lit-plugin",
|
||||
"github.vscode-pull-request-github",
|
||||
"eamodio.gitlens"
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.rulers": [80],
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.shell.linux": "/usr/bin/zsh",
|
||||
"gitlens.showWelcomeOnInstall": false,
|
||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
||||
"workbench.startupEditor": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -6,3 +6,9 @@ updates:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 5
|
||||
|
90
.github/workflows/cast_deployment.yaml
vendored
Normal file
90
.github/workflows/cast_deployment.yaml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Cast deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
deploy_dev:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Development
|
||||
if: github.event_name != 'push'
|
||||
environment:
|
||||
name: Cast Development
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build Cast
|
||||
run: ./node_modules/.bin/gulp build-cast
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=cast/dist --alias dev
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
||||
|
||||
deploy_master:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy Production
|
||||
if: github.event_name == 'push'
|
||||
environment:
|
||||
name: Cast Production
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build Cast
|
||||
run: ./node_modules/.bin/gulp build-cast
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=cast/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
|
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
@@ -82,9 +82,9 @@ jobs:
|
||||
needs: [lint, test]
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
91
.github/workflows/demo_deployment.yaml
vendored
Normal file
91
.github/workflows/demo_deployment.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Demo deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
deploy_dev:
|
||||
runs-on: ubuntu-latest
|
||||
name: Demo Development
|
||||
if: github.event_name != 'push' || github.ref != 'master'
|
||||
environment:
|
||||
name: Demo Development
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||
|
||||
deploy_master:
|
||||
runs-on: ubuntu-latest
|
||||
name: Demo Production
|
||||
if: github.event_name == 'push' && github.ref == 'master'
|
||||
environment:
|
||||
name: Demo Production
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=demo/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}
|
@@ -1,9 +1,9 @@
|
||||
name: Demo
|
||||
name: Design deployment
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
@@ -12,24 +12,34 @@ env:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: Design
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
- name: Build Demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
|
||||
- name: Build Gallery
|
||||
run: ./node_modules/.bin/gulp build-gallery
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
run: npx netlify-cli deploy --dir=demo/dist --prod
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=gallery/dist --prod
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
54
.github/workflows/design_preview.yaml
vendored
Normal file
54
.github/workflows/design_preview.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Design preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip running on forks since it won't have access to secrets
|
||||
# Skip running PRs without 'needs design preview' label
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build Gallery
|
||||
run: ./node_modules/.bin/gulp build-gallery
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy preview to Netlify
|
||||
id: deploy
|
||||
uses: netlify/actions/cli@master
|
||||
with:
|
||||
args: deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}"
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
|
||||
|
||||
- name: Generate summary
|
||||
run: |
|
||||
echo "${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}" >> "$GITHUB_STEP_SUMMARY"
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3.0.0
|
||||
- uses: dessant/lock-threads@v4.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
19
.github/workflows/netflify.yml
vendored
19
.github/workflows/netflify.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Netlify
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
trigger_builds:
|
||||
name: Trigger netlify build preview
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Trigger Cast build
|
||||
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_CAST_DEV_BUILD_HOOK }}
|
||||
|
||||
- name: Trigger Demo build
|
||||
run: curl -X POST -d {} https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_DEMO_DEV_BUILD_HOOK }}
|
||||
|
||||
- name: Trigger Design build
|
||||
run: curl -X POST -d "NIGHTLY" https://api.netlify.com/build_hooks/${{ secrets.NETLIFY_GALLERY_DEV_BUILD_HOOK }}
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v6.0.1
|
||||
uses: actions/stale@v7.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
14
.gitignore
vendored
14
.gitignore
vendored
@@ -2,10 +2,10 @@
|
||||
.reify-cache
|
||||
|
||||
# build
|
||||
build
|
||||
hass_frontend/*
|
||||
dist
|
||||
translations
|
||||
build/
|
||||
dist/
|
||||
/hass_frontend/
|
||||
/translations/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
@@ -15,7 +15,7 @@ translations
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/*
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
@@ -27,7 +27,7 @@ npm-debug.log
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv/*
|
||||
/venv/
|
||||
.venv
|
||||
|
||||
# vscode
|
||||
@@ -46,4 +46,4 @@ src/cast/dev_const.ts
|
||||
.tool-versions
|
||||
|
||||
# Home Assistant config
|
||||
/config
|
||||
/config/
|
||||
|
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -2,7 +2,8 @@
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"bierner.lit-html",
|
||||
"runem.lit-plugin"
|
||||
"runem.lit-plugin",
|
||||
"github.vscode-pull-request-github",
|
||||
"eamodio.gitlens"
|
||||
]
|
||||
}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
diff --git a/mwc-icon-button-base.js b/mwc-icon-button-base.js
|
||||
index 45cdaab93ccc0a6daaaaabc01266dcdc32e46bfd..b3ea5b541597308d85f86ce6c23fd00785fda835 100644
|
||||
--- a/mwc-icon-button-base.js
|
||||
+++ b/mwc-icon-button-base.js
|
||||
@@ -63,7 +63,6 @@ export class IconButtonBase extends LitElement {
|
||||
@touchend="${this.handleRippleDeactivate}"
|
||||
@touchcancel="${this.handleRippleDeactivate}"
|
||||
>${this.renderRipple()}
|
||||
- <i class="material-icons">${this.icon}</i>
|
||||
<span
|
||||
><slot></slot
|
||||
></span>
|
@@ -1,36 +1,40 @@
|
||||
const del = require("del");
|
||||
const del = import("del");
|
||||
const gulp = require("gulp");
|
||||
const paths = require("../paths");
|
||||
require("./translations");
|
||||
|
||||
gulp.task(
|
||||
"clean",
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.app_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.app_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-demo",
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.demo_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.demo_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-cast",
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.cast_output_root, paths.build_dir])
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([paths.cast_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("clean-hassio", () =>
|
||||
del([paths.hassio_output_root, paths.build_dir])
|
||||
gulp.task("clean-hassio", async () =>
|
||||
(await del).deleteSync([paths.hassio_output_root, paths.build_dir])
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-gallery",
|
||||
gulp.parallel("clean-translations", () =>
|
||||
del([paths.gallery_output_root, paths.gallery_build, paths.build_dir])
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
(await del).deleteSync([
|
||||
paths.gallery_output_root,
|
||||
paths.gallery_build,
|
||||
paths.build_dir,
|
||||
])
|
||||
)
|
||||
);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// Task to download the latest Lokalise translations from the nightly workflow artifacts
|
||||
|
||||
const del = import("del");
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const del = require("del");
|
||||
const gulp = require("gulp");
|
||||
const jszip = require("jszip");
|
||||
const tar = require("tar");
|
||||
@@ -17,8 +17,8 @@ const WORKFLOW_NAME = "nightly.yaml";
|
||||
const ARTIFACT_NAME = "translations";
|
||||
const CLIENT_ID = "Iv1.3914e28cb27834d1";
|
||||
const EXTRACT_DIR = "translations";
|
||||
const TOKEN_FILE = path.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.join(EXTRACT_DIR, "artifact.json");
|
||||
const TOKEN_FILE = path.posix.join(EXTRACT_DIR, "token.json");
|
||||
const ARTIFACT_FILE = path.posix.join(EXTRACT_DIR, "artifact.json");
|
||||
|
||||
let allowTokenSetup = false;
|
||||
gulp.task("allow-setup-fetch-nightly-translations", (done) => {
|
||||
@@ -137,7 +137,11 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
|
||||
// Remove the current translations
|
||||
const deleteCurrent = Promise.all(writings).then(
|
||||
del([`${EXTRACT_DIR}/*`, `!${ARTIFACT_FILE}`, `!${TOKEN_FILE}`])
|
||||
(await del).deleteAsync([
|
||||
`${EXTRACT_DIR}/*`,
|
||||
`!${ARTIFACT_FILE}`,
|
||||
`!${TOKEN_FILE}`,
|
||||
])
|
||||
);
|
||||
|
||||
// Get the download URL and follow the redirect to download (stored as ArrayBuffer)
|
@@ -1,4 +1,4 @@
|
||||
const del = require("del");
|
||||
const del = import("del");
|
||||
const path = require("path");
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
@@ -6,7 +6,7 @@ const paths = require("../paths");
|
||||
|
||||
const outDir = "build/locale-data";
|
||||
|
||||
gulp.task("clean-locale-data", () => del([outDir]));
|
||||
gulp.task("clean-locale-data", async () => (await del).deleteSync([outDir]));
|
||||
|
||||
gulp.task("ensure-locale-data-build-dir", (done) => {
|
||||
if (!fs.existsSync(outDir)) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const del = import("del");
|
||||
const crypto = require("crypto");
|
||||
const del = require("del");
|
||||
const path = require("path");
|
||||
const source = require("vinyl-source-stream");
|
||||
const vinylBuffer = require("vinyl-buffer");
|
||||
@@ -13,7 +13,7 @@ const { mapFiles } = require("../util");
|
||||
const env = require("../env");
|
||||
const paths = require("../paths");
|
||||
|
||||
require("./fetch-nightly_translations");
|
||||
require("./fetch-nightly-translations");
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
@@ -120,7 +120,7 @@ function lokaliseTransform(data, original, file) {
|
||||
return output;
|
||||
}
|
||||
|
||||
gulp.task("clean-translations", () => del([workDir]));
|
||||
gulp.task("clean-translations", async () => (await del).deleteSync([workDir]));
|
||||
|
||||
gulp.task("ensure-translations-build-dir", (done) => {
|
||||
if (!fs.existsSync(workDir)) {
|
||||
|
@@ -181,7 +181,7 @@ class HcCast extends LitElement {
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, path);
|
||||
castSendShowLovelaceView(this.castManager, path, this.auth.data.hassUrl);
|
||||
}
|
||||
|
||||
private async _handleLogout() {
|
||||
|
@@ -33,7 +33,6 @@ import { castContext } from "../cast_context";
|
||||
import "./hc-launch-screen";
|
||||
|
||||
let resourcesLoaded = false;
|
||||
|
||||
@customElement("hc-main")
|
||||
export class HcMain extends HassElement {
|
||||
@state() private _showDemo = false;
|
||||
@@ -46,6 +45,8 @@ export class HcMain extends HassElement {
|
||||
|
||||
@state() private _urlPath?: string | null;
|
||||
|
||||
private _hassUUID?: string;
|
||||
|
||||
private _unsubLovelace?: UnsubscribeFunc;
|
||||
|
||||
public processIncomingMessage(msg: HassMessage) {
|
||||
@@ -125,6 +126,7 @@ export class HcMain extends HassElement {
|
||||
|
||||
if (this.hass) {
|
||||
status.hassUrl = this.hass.auth.data.hassUrl;
|
||||
status.hassUUID = this._hassUUID;
|
||||
status.lovelacePath = this._lovelacePath;
|
||||
status.urlPath = this._urlPath;
|
||||
}
|
||||
@@ -163,6 +165,18 @@ export class HcMain extends HassElement {
|
||||
};
|
||||
|
||||
private async _handleGetStatusMessage(msg: GetStatusMessage) {
|
||||
if (
|
||||
(this.hass && msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
|
||||
(this.hass && msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
|
||||
) {
|
||||
this._error = "Not connected to the same Home Assistant instance.";
|
||||
this._sendError(
|
||||
ReceiverErrorCode.WRONG_INSTANCE,
|
||||
this._error,
|
||||
msg.senderId!
|
||||
);
|
||||
}
|
||||
|
||||
this._sendStatus(msg.senderId!);
|
||||
}
|
||||
|
||||
@@ -179,6 +193,7 @@ export class HcMain extends HassElement {
|
||||
expires_in: 0,
|
||||
}),
|
||||
});
|
||||
this._hassUUID = msg.hassUUID;
|
||||
} catch (err: any) {
|
||||
const errorMessage = this._getErrorMessage(err);
|
||||
this._error = errorMessage;
|
||||
@@ -209,9 +224,29 @@ export class HcMain extends HassElement {
|
||||
if (!this.hass) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
this._error = "Cannot show Lovelace because we're not connected.";
|
||||
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
|
||||
this._sendError(
|
||||
ReceiverErrorCode.NOT_CONNECTED,
|
||||
this._error,
|
||||
msg.senderId!
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(msg.hassUUID && msg.hassUUID !== this._hassUUID) ||
|
||||
(msg.hassUrl && msg.hassUrl !== this.hass.auth.data.hassUrl)
|
||||
) {
|
||||
this._sendStatus(msg.senderId!);
|
||||
this._error =
|
||||
"Cannot show Lovelace because we're not connected to the same Home Assistant instance.";
|
||||
this._sendError(
|
||||
ReceiverErrorCode.WRONG_INSTANCE,
|
||||
this._error,
|
||||
msg.senderId!
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
if (msg.urlPath === "lovelace") {
|
||||
msg.urlPath = null;
|
||||
|
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET_LABEL="needs design preview"
|
||||
|
||||
if [[ "$NETLIFY" != "true" ]]; then
|
||||
echo "This script can only be run on Netlify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function createStatus() {
|
||||
state="$1"
|
||||
description="$2"
|
||||
target_url="$3"
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \
|
||||
-d '{"state": "'"${state}"'", "context": "Netlify/Design Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}'
|
||||
}
|
||||
|
||||
|
||||
if [[ "${PULL_REQUEST}" == "true" ]]; then
|
||||
if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then
|
||||
createStatus "pending" "Building design preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
gulp build-gallery
|
||||
if [ $? -eq 0 ]; then
|
||||
createStatus "success" "Build complete" "$DEPLOY_PRIME_URL"
|
||||
else
|
||||
createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
|
||||
fi
|
||||
else
|
||||
createStatus "success" "Build was not requested by PR label"
|
||||
fi
|
||||
elif [[ "$INCOMING_HOOK_BODY" == "NIGHTLY" ]]; then
|
||||
gulp build-gallery
|
||||
fi
|
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Alerts
|
||||
title: Alert
|
||||
subtitle: An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
|
||||
---
|
||||
|
||||
|
@@ -98,7 +98,9 @@ const alerts: {
|
||||
description: "Alert with slotted image",
|
||||
type: "warning",
|
||||
iconSlot: html`<span slot="icon" class="image"
|
||||
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
|
||||
><img
|
||||
alt="Home Assistant logo"
|
||||
src="https://www.home-assistant.io/images/home-assistant-logo.svg"
|
||||
/></span>`,
|
||||
},
|
||||
{
|
||||
|
@@ -142,7 +142,8 @@ export class DemoHaBarSlider extends LitElement {
|
||||
}
|
||||
.custom {
|
||||
--slider-bar-color: #ffcf4c;
|
||||
--slider-bar-background: #ffcf4c64;
|
||||
--slider-bar-background: #ffcf4c;
|
||||
--slider-bar-background-opacity: 0.2;
|
||||
--slider-bar-thickness: 100px;
|
||||
--slider-bar-border-radius: 24px;
|
||||
}
|
||||
|
@@ -115,8 +115,8 @@ export class DemoHaBarSwitch extends LitElement {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
--switch-bar-color-on: var(--rgb-green-color);
|
||||
--switch-bar-color-off: var(--rgb-red-color);
|
||||
--switch-bar-on-color: rgb(var(--rgb-green-color));
|
||||
--switch-bar-off-color: rgb(var(--rgb-red-color));
|
||||
--switch-bar-thickness: 100px;
|
||||
--switch-bar-border-radius: 24px;
|
||||
--switch-bar-padding: 6px;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
---
|
||||
title: Progress Bars
|
||||
title: Bar
|
||||
subtitle: Can be used to communicate progress of a task.
|
||||
---
|
||||
|
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Selectors
|
||||
title: Selector
|
||||
---
|
||||
|
||||
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).
|
||||
|
@@ -142,6 +142,25 @@ const CONFIGS = [
|
||||
heading: "Basic",
|
||||
config: `
|
||||
- type: entities
|
||||
entities:
|
||||
- scene.romantic_lights
|
||||
- device_tracker.demo_paulus
|
||||
- cover.kitchen_window
|
||||
- group.kitchen
|
||||
- lock.kitchen_door
|
||||
- light.bed_light
|
||||
- light.non_existing
|
||||
- climate.ecobee
|
||||
- input_number.number
|
||||
- sensor.humidity
|
||||
- text.message
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "With enabled state color",
|
||||
config: `
|
||||
- type: entities
|
||||
state_color: true
|
||||
entities:
|
||||
- scene.romantic_lights
|
||||
- device_tracker.demo_paulus
|
||||
|
@@ -35,11 +35,11 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Without State",
|
||||
heading: "With State",
|
||||
config: `
|
||||
- type: button
|
||||
entity: light.bed_light
|
||||
show_state: false
|
||||
show_state: true
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
@@ -62,6 +62,21 @@ const CONFIGS = [
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: glance
|
||||
entities:
|
||||
- device_tracker.demo_paulus
|
||||
- media_player.living_room
|
||||
- sun.sun
|
||||
- cover.kitchen_window
|
||||
- light.kitchen_lights
|
||||
- lock.kitchen_door
|
||||
- light.ceiling_lights
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "No state colors",
|
||||
config: `
|
||||
- type: glance
|
||||
state_color: false
|
||||
entities:
|
||||
- device_tracker.demo_paulus
|
||||
- media_player.living_room
|
||||
|
@@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Grid And Stack Card
|
||||
title: Grid and Stack Card
|
||||
---
|
||||
|
@@ -4,14 +4,12 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../../src/common/entity/compute_domain";
|
||||
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
|
||||
import { stateColorCss } from "../../../../src/common/entity/state_color";
|
||||
import { stateIconPath } from "../../../../src/common/entity/state_icon_path";
|
||||
import "../../../../src/components/data-table/ha-data-table";
|
||||
import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table";
|
||||
import "../../../../src/components/entity/state-badge";
|
||||
import "../../../../src/components/ha-chip";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
@@ -105,6 +103,13 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("alarm_control_panel.arming", "arming"),
|
||||
createEntity("alarm_control_panel.disarming", "disarming"),
|
||||
createEntity("alarm_control_panel.triggered", "triggered"),
|
||||
// Alert
|
||||
createEntity("alert.off", "off"),
|
||||
createEntity("alert.on", "on"),
|
||||
createEntity("alert.idle", "idle"),
|
||||
// Automation
|
||||
createEntity("automation.off", "off"),
|
||||
createEntity("automation.on", "on"),
|
||||
// Binary Sensor
|
||||
...BINARY_SENSOR_DEVICE_CLASSES.map((dc) =>
|
||||
createEntity(`binary_sensor.${dc}`, "on", dc)
|
||||
@@ -113,8 +118,11 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("button.restart", "unknown", "restart"),
|
||||
createEntity("button.update", "unknown", "update"),
|
||||
// Calendar
|
||||
createEntity("calendar.on", "on"),
|
||||
createEntity("calendar.off", "off"),
|
||||
createEntity("calendar.on", "on"),
|
||||
// Camera
|
||||
createEntity("camera.idle", "idle"),
|
||||
createEntity("camera.streaming", "streaming"),
|
||||
// Climate
|
||||
createEntity("climate.off", "off"),
|
||||
createEntity("climate.heat", "heat"),
|
||||
@@ -123,11 +131,22 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("climate.auto", "auto"),
|
||||
createEntity("climate.dry", "dry"),
|
||||
createEntity("climate.fan_only", "fan_only"),
|
||||
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
|
||||
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
|
||||
createEntity("climate.auto_heating", "auto", undefined, {
|
||||
hvac_action: "heating",
|
||||
}),
|
||||
createEntity("climate.auto_cooling", "auto", undefined, {
|
||||
hvac_action: "cooling",
|
||||
}),
|
||||
createEntity("climate.auto_dry", "auto", undefined, {
|
||||
hvac_action: "drying",
|
||||
}),
|
||||
// Cover
|
||||
createEntity("cover.opening", "opening"),
|
||||
createEntity("cover.open", "open"),
|
||||
createEntity("cover.closing", "closing"),
|
||||
createEntity("cover.closed", "closed"),
|
||||
createEntity("cover.opening", "opening"),
|
||||
createEntity("cover.open", "open"),
|
||||
createEntity("cover.awning", "open", "awning"),
|
||||
createEntity("cover.blind", "open", "blind"),
|
||||
createEntity("cover.curtain", "open", "curtain"),
|
||||
@@ -139,24 +158,30 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("cover.shutter", "open", "shutter"),
|
||||
createEntity("cover.window", "open", "window"),
|
||||
// Device tracker/person
|
||||
createEntity("device_tracker.home", "home"),
|
||||
createEntity("device_tracker.not_home", "not_home"),
|
||||
createEntity("device_tracker.home", "home"),
|
||||
createEntity("device_tracker.work", "work"),
|
||||
createEntity("person.home", "home"),
|
||||
createEntity("person.not_home", "not_home"),
|
||||
createEntity("person.work", "work"),
|
||||
// Fan
|
||||
createEntity("fan.on", "on"),
|
||||
createEntity("fan.off", "off"),
|
||||
createEntity("fan.on", "on"),
|
||||
// Camera
|
||||
createEntity("group.off", "off"),
|
||||
createEntity("group.on", "on"),
|
||||
// Humidifier
|
||||
createEntity("humidifier.on", "on"),
|
||||
createEntity("humidifier.off", "off"),
|
||||
createEntity("humidifier.on", "on"),
|
||||
// Helpers
|
||||
createEntity("input_boolean.off", "off"),
|
||||
createEntity("input_boolean.on", "on"),
|
||||
// Light
|
||||
createEntity("light.on", "on"),
|
||||
createEntity("light.off", "off"),
|
||||
createEntity("light.on", "on"),
|
||||
// Locks
|
||||
createEntity("lock.locked", "locked"),
|
||||
createEntity("lock.unlocked", "unlocked"),
|
||||
createEntity("lock.locked", "locked"),
|
||||
createEntity("lock.locking", "locking"),
|
||||
createEntity("lock.unlocking", "unlocking"),
|
||||
createEntity("lock.jammed", "jammed"),
|
||||
@@ -180,6 +205,12 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("media_player.speaker_playing", "playing", "speaker"),
|
||||
createEntity("media_player.speaker_paused", "paused", "speaker"),
|
||||
createEntity("media_player.speaker_standby", "standby", "speaker"),
|
||||
// Remote
|
||||
createEntity("remote.off", "off"),
|
||||
createEntity("remote.on", "on"),
|
||||
// Script
|
||||
createEntity("script.off", "off"),
|
||||
createEntity("script.on", "on"),
|
||||
// Sensor
|
||||
...SENSOR_DEVICE_CLASSES.map((dc) => createEntity(`sensor.${dc}`, "10", dc)),
|
||||
// Battery sensor
|
||||
@@ -189,6 +220,11 @@ const ENTITIES: HassEntity[] = [
|
||||
// Siren
|
||||
createEntity("siren.off", "off"),
|
||||
createEntity("siren.on", "on"),
|
||||
// Sun
|
||||
createEntity("sun.below", "below_horizon"),
|
||||
createEntity("sun.above", "above_horizon"),
|
||||
createEntity("sun.unknown", "unknown"),
|
||||
createEntity("sun.unavailable", "unavailable"),
|
||||
// Switch
|
||||
createEntity("switch.off", "off"),
|
||||
createEntity("switch.on", "on"),
|
||||
@@ -196,9 +232,13 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("switch.outlet_on", "on", "outlet"),
|
||||
createEntity("switch.switch_off", "off", "switch"),
|
||||
createEntity("switch.switch_on", "on", "switch"),
|
||||
// Timer
|
||||
createEntity("timer.idle", "idle"),
|
||||
createEntity("timer.active", "active"),
|
||||
createEntity("timer.paused", "paused"),
|
||||
// Vacuum
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.docked", "docked"),
|
||||
createEntity("vacuum.cleaning", "cleaning"),
|
||||
createEntity("vacuum.paused", "paused"),
|
||||
createEntity("vacuum.idle", "idle"),
|
||||
createEntity("vacuum.returning", "returning"),
|
||||
@@ -280,21 +320,15 @@ export class DemoEntityState extends LitElement {
|
||||
const columns: DataTableColumnContainer<EntityRowData> = {
|
||||
icon: {
|
||||
title: "Icon",
|
||||
template: (_, entry) => {
|
||||
const cssColor = stateColorCss(entry.stateObj);
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
style=${styleMap({
|
||||
color: `rgb(${cssColor})`,
|
||||
})}
|
||||
.path=${stateIconPath(entry.stateObj)}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`;
|
||||
},
|
||||
template: (_, entry) => html`
|
||||
<state-badge
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
`,
|
||||
},
|
||||
entity_id: {
|
||||
title: "Entity id",
|
||||
title: "Entity ID",
|
||||
width: "30%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
0
hassio/.gitignore
vendored
0
hassio/.gitignore
vendored
@@ -29,7 +29,9 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
if (filter) {
|
||||
return filterAndSort(addons, filter);
|
||||
}
|
||||
return addons.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name));
|
||||
return addons.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@@ -404,6 +404,7 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src="/api/hassio/addons/${this.addon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
|
@@ -35,7 +35,13 @@ class HassioAddons extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: this.supervisor.addon.addons
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.name,
|
||||
b.name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
.map(
|
||||
(addon) => html`
|
||||
<ha-card
|
||||
|
@@ -28,6 +28,7 @@ class HassioDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
>
|
||||
<hassio-addons
|
||||
|
@@ -15,7 +15,12 @@ import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
|
||||
|
||||
const _filterDevices = memoizeOne(
|
||||
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
|
||||
(
|
||||
showAdvanced: boolean,
|
||||
hardware: HassioHardwareInfo,
|
||||
filter: string,
|
||||
language: string
|
||||
) =>
|
||||
hardware.devices
|
||||
.filter(
|
||||
(device) =>
|
||||
@@ -28,7 +33,7 @@ const _filterDevices = memoizeOne(
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name))
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
@customElement("dialog-hassio-hardware")
|
||||
@@ -56,7 +61,8 @@ class HassioHardwareDialog extends LitElement {
|
||||
const devices = _filterDevices(
|
||||
this.hass.userData?.showAdvanced || false,
|
||||
this._dialogParams.hardware,
|
||||
(this._filter || "").toLowerCase()
|
||||
(this._filter || "").toLowerCase(),
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
|
@@ -68,7 +68,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "5c53de3b" // The ESPHome repository
|
||||
)
|
||||
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
)
|
||||
);
|
||||
|
||||
private _filteredUsedRepositories = memoizeOne(
|
||||
|
@@ -59,7 +59,11 @@ class HassioIngressView extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
|
||||
const iframe = html`<iframe
|
||||
.title=${this._addon.name}
|
||||
.src=${this._addon.ingress_url!}
|
||||
>
|
||||
</iframe>`;
|
||||
|
||||
if (!this.ingressPanel) {
|
||||
return html`<hass-subpage
|
||||
|
@@ -5,4 +5,5 @@ module.exports = {
|
||||
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
|
||||
files.join(" ") +
|
||||
" >&2 && exit 1",
|
||||
"/yarn.lock": () => "yarn dedupe",
|
||||
};
|
||||
|
142
package.json
142
package.json
@@ -24,22 +24,17 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
"@codemirror/autocomplete": "^0.19.12",
|
||||
"@codemirror/commands": "^0.19.8",
|
||||
"@codemirror/gutter": "^0.19.9",
|
||||
"@codemirror/highlight": "^0.19.7",
|
||||
"@codemirror/history": "^0.19.2",
|
||||
"@codemirror/legacy-modes": "^0.19.0",
|
||||
"@codemirror/rectangular-selection": "^0.19.1",
|
||||
"@codemirror/search": "^0.19.6",
|
||||
"@codemirror/state": "^0.19.6",
|
||||
"@codemirror/stream-parser": "^0.19.5",
|
||||
"@codemirror/text": "^0.19.6",
|
||||
"@codemirror/view": "^0.19.40",
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/commands": "^6.1.3",
|
||||
"@codemirror/language": "^6.3.2",
|
||||
"@codemirror/legacy-modes": "^6.3.1",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.7.1",
|
||||
"@formatjs/intl-datetimeformat": "^4.2.5",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
||||
"@formatjs/intl-locale": "^2.4.40",
|
||||
"@formatjs/intl-locale": "^3.0.11",
|
||||
"@formatjs/intl-numberformat": "^7.2.5",
|
||||
"@formatjs/intl-pluralrules": "^4.1.5",
|
||||
"@formatjs/intl-relativetimeformat": "^9.3.2",
|
||||
@@ -49,34 +44,35 @@
|
||||
"@fullcalendar/interaction": "5.9.0",
|
||||
"@fullcalendar/list": "5.9.0",
|
||||
"@fullcalendar/timegrid": "5.9.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lit-labs/motion": "^1.0.2",
|
||||
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.7.0-pre.2#./.yarn/patches/@lit-labs/virtualizer/event-target-shim.patch",
|
||||
"@material/chips": "14.0.0-canary.261f2db59.0",
|
||||
"@material/data-table": "14.0.0-canary.261f2db59.0",
|
||||
"@material/mwc-button": "0.25.3",
|
||||
"@material/mwc-checkbox": "0.25.3",
|
||||
"@material/mwc-circular-progress": "0.25.3",
|
||||
"@material/mwc-dialog": "0.25.3",
|
||||
"@material/mwc-drawer": "^0.25.3",
|
||||
"@material/mwc-fab": "0.25.3",
|
||||
"@material/mwc-formfield": "0.25.3",
|
||||
"@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
|
||||
"@material/mwc-linear-progress": "0.25.3",
|
||||
"@material/mwc-list": "^0.25.3",
|
||||
"@material/mwc-menu": "0.25.3",
|
||||
"@material/mwc-radio": "0.25.3",
|
||||
"@material/mwc-ripple": "0.25.3",
|
||||
"@material/mwc-select": "0.25.3",
|
||||
"@material/mwc-slider": "0.25.3",
|
||||
"@material/mwc-switch": "0.25.3",
|
||||
"@material/mwc-tab": "0.25.3",
|
||||
"@material/mwc-tab-bar": "0.25.3",
|
||||
"@material/mwc-textarea": "^0.25.3",
|
||||
"@material/mwc-textfield": "0.25.3",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.25.3",
|
||||
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
|
||||
"@mdi/js": "7.0.96",
|
||||
"@mdi/svg": "7.0.96",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "^0.27.0",
|
||||
"@material/mwc-checkbox": "^0.27.0",
|
||||
"@material/mwc-circular-progress": "^0.27.0",
|
||||
"@material/mwc-dialog": "^0.27.0",
|
||||
"@material/mwc-drawer": "^0.27.0",
|
||||
"@material/mwc-fab": "^0.27.0",
|
||||
"@material/mwc-formfield": "^0.27.0",
|
||||
"@material/mwc-icon-button": "^0.27.0",
|
||||
"@material/mwc-linear-progress": "^0.27.0",
|
||||
"@material/mwc-list": "^0.27.0",
|
||||
"@material/mwc-menu": "^0.27.0",
|
||||
"@material/mwc-radio": "^0.27.0",
|
||||
"@material/mwc-ripple": "^0.27.0",
|
||||
"@material/mwc-select": "^0.27.0",
|
||||
"@material/mwc-slider": "^0.27.0",
|
||||
"@material/mwc-switch": "^0.27.0",
|
||||
"@material/mwc-tab": "^0.27.0",
|
||||
"@material/mwc-tab-bar": "^0.27.0",
|
||||
"@material/mwc-textarea": "^0.27.0",
|
||||
"@material/mwc-textfield": "^0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "^0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@mdi/js": "7.1.96",
|
||||
"@mdi/svg": "7.1.96",
|
||||
"@polymer/app-layout": "^3.1.0",
|
||||
"@polymer/iron-flex-layout": "^3.0.1",
|
||||
"@polymer/iron-icon": "^3.0.1",
|
||||
@@ -91,27 +87,28 @@
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.4.1",
|
||||
"@thomasloven/round-slider": "0.5.4",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "^23.2.9",
|
||||
"@vaadin/vaadin-themable-mixin": "^23.2.9",
|
||||
"@vibrant/color": "^3.2.1-alpha.1",
|
||||
"@vibrant/core": "^3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
|
||||
"@vue/web-component-wrapper": "^1.2.0",
|
||||
"@vue/web-component-wrapper": "^1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "^0.0.5",
|
||||
"@webcomponents/webcomponentsjs": "^2.2.10",
|
||||
"app-datepicker": "^5.0.1",
|
||||
"app-datepicker": "^5.1.0",
|
||||
"chart.js": "^3.3.2",
|
||||
"comlink": "^4.3.1",
|
||||
"core-js": "^3.15.2",
|
||||
"cropperjs": "^1.5.12",
|
||||
"date-fns": "^2.23.0",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deep-clone-simple": "^1.1.1",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"hls.js": "^1.2.5",
|
||||
"hls.js": "^1.3.1",
|
||||
"home-assistant-js-websocket": "^8.0.1",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
@@ -120,13 +117,13 @@
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"lit": "^2.1.2",
|
||||
"marked": "^4.0.12",
|
||||
"memoize-one": "^5.2.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
"punycode": "^2.1.1",
|
||||
"qr-scanner": "^1.3.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"rrule": "^2.7.1",
|
||||
@@ -140,19 +137,19 @@
|
||||
"vue": "^2.6.12",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
"weekstart": "^1.1.0",
|
||||
"workbox-cacheable-response": "^6.4.2",
|
||||
"workbox-core": "^6.4.2",
|
||||
"workbox-expiration": "^6.4.2",
|
||||
"workbox-precaching": "^6.4.2",
|
||||
"workbox-routing": "^6.4.2",
|
||||
"workbox-strategies": "^6.4.2",
|
||||
"xss": "^1.0.9"
|
||||
"workbox-cacheable-response": "^6.5.4",
|
||||
"workbox-core": "^6.5.4",
|
||||
"workbox-expiration": "^6.5.4",
|
||||
"workbox-precaching": "^6.5.4",
|
||||
"workbox-routing": "^6.5.4",
|
||||
"workbox-strategies": "^6.5.4",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/plugin-external-helpers": "^7.18.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.2",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.7",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.2",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9",
|
||||
@@ -163,7 +160,7 @@
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@octokit/auth-oauth-device": "^4.0.2",
|
||||
"@octokit/rest": "^19.0.4",
|
||||
"@octokit/rest": "^19.0.5",
|
||||
"@open-wc/dev-server-hmr": "^0.0.2",
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
@@ -172,7 +169,7 @@
|
||||
"@rollup/plugin-replace": "^2.3.2",
|
||||
"@types/chromecast-caf-receiver": "5.0.12",
|
||||
"@types/chromecast-caf-sender": "^1.0.3",
|
||||
"@types/glob": "^7",
|
||||
"@types/glob": "^8",
|
||||
"@types/hammerjs": "^2.0.41",
|
||||
"@types/js-yaml": "^4",
|
||||
"@types/leaflet": "^1",
|
||||
@@ -183,27 +180,26 @@
|
||||
"@types/sortablejs": "^1",
|
||||
"@types/tar": "^6",
|
||||
"@types/webspeechapi": "^0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"@web/dev-server": "^0.0.24",
|
||||
"@web/dev-server-rollup": "^0.2.11",
|
||||
"babel-loader": "^9.1.0",
|
||||
"chai": "^4.3.4",
|
||||
"del": "^4.0.0",
|
||||
"del": "^7.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-airbnb-typescript": "^14.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-webpack": "^0.13.1",
|
||||
"eslint-plugin-disable": "^2.0.1",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^1.1.5",
|
||||
"eslint-plugin-wc": "^1.3.2",
|
||||
"fancy-log": "^1.3.3",
|
||||
"fs-extra": "^7.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"glob": "^8.1.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-flatmap": "^1.0.2",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
@@ -214,38 +210,38 @@
|
||||
"husky": "^8.0.1",
|
||||
"instant-mocha": "^1.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"lit-analyzer": "^1.2.1",
|
||||
"lodash.template": "^4.5.0",
|
||||
"magic-string": "^0.25.7",
|
||||
"map-stream": "^0.0.7",
|
||||
"merge-stream": "^1.0.1",
|
||||
"mocha": "^8.4.0",
|
||||
"object-hash": "^2.0.3",
|
||||
"open": "^7.0.4",
|
||||
"object-hash": "^3.0.0",
|
||||
"open": "^8.4.0",
|
||||
"pinst": "^3.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier": "^2.8.3",
|
||||
"require-dir": "^1.2.0",
|
||||
"rollup": "^2.8.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"rollup-plugin-visualizer": "^4.0.4",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"serve": "^11.3.2",
|
||||
"sinon": "^11.0.0",
|
||||
"sinon": "^15.0.1",
|
||||
"source-map-url": "^0.4.0",
|
||||
"systemjs": "^6.3.2",
|
||||
"tar": "^6.1.11",
|
||||
"terser-webpack-plugin": "^5.2.4",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"typescript": "^4.9.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "^5.55.1",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.3.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-manifest-plugin": "^4.0.2",
|
||||
"webpackbar": "^5.0.0-3",
|
||||
"workbox-build": "^6.4.2"
|
||||
"workbox-build": "^6.5.4"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20221201.1"
|
||||
version = "20230104.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -8,6 +8,8 @@ import { BaseCastMessage } from "./types";
|
||||
|
||||
export interface GetStatusMessage extends BaseCastMessage {
|
||||
type: "get_status";
|
||||
hassUrl?: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ConnectMessage extends BaseCastMessage {
|
||||
@@ -15,12 +17,15 @@ export interface ConnectMessage extends BaseCastMessage {
|
||||
refreshToken: string;
|
||||
clientId: string | null;
|
||||
hassUrl: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ShowLovelaceViewMessage extends BaseCastMessage {
|
||||
type: "show_lovelace_view";
|
||||
viewPath: string | number | null;
|
||||
urlPath: string | null;
|
||||
hassUrl: string;
|
||||
hassUUID?: string;
|
||||
}
|
||||
|
||||
export interface ShowDemoMessage extends BaseCastMessage {
|
||||
@@ -43,6 +48,7 @@ export const castSendAuth = (cast: CastManager, auth: Auth) =>
|
||||
|
||||
export const castSendShowLovelaceView = (
|
||||
cast: CastManager,
|
||||
hassUrl: string,
|
||||
viewPath: ShowLovelaceViewMessage["viewPath"],
|
||||
urlPath?: string | null
|
||||
) =>
|
||||
@@ -50,6 +56,7 @@ export const castSendShowLovelaceView = (
|
||||
type: "show_lovelace_view",
|
||||
viewPath,
|
||||
urlPath: urlPath || null,
|
||||
hassUrl: CAST_DEV ? CAST_DEV_HASS_URL : hassUrl,
|
||||
});
|
||||
|
||||
export const castSendShowDemo = (cast: CastManager) =>
|
||||
|
@@ -7,6 +7,7 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
|
||||
connected: boolean;
|
||||
showDemo: boolean;
|
||||
hassUrl?: string;
|
||||
hassUUID?: string;
|
||||
lovelacePath?: string | number | null;
|
||||
urlPath?: string | null;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ export const enum ReceiverErrorCode {
|
||||
CONNECTION_LOST = 3,
|
||||
HASS_URL_MISSING = 4,
|
||||
NO_HTTPS = 5,
|
||||
WRONG_INSTANCE = 20,
|
||||
NOT_CONNECTED = 21,
|
||||
FETCH_CONFIG_FAILED = 22,
|
||||
}
|
||||
|
5
src/common/array/literal-includes.ts
Normal file
5
src/common/array/literal-includes.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Creates a type predicate function for determining if an array literal includes a given value
|
||||
export const arrayLiteralIncludes =
|
||||
<T extends readonly unknown[]>(array: T) =>
|
||||
(searchElement: unknown, fromIndex?: number): searchElement is T[number] =>
|
||||
array.includes(searchElement as T[number], fromIndex);
|
@@ -1,5 +1,3 @@
|
||||
import { hex2rgb } from "./convert-color";
|
||||
|
||||
export const THEME_COLORS = new Set([
|
||||
"primary",
|
||||
"accent",
|
||||
@@ -27,16 +25,9 @@ export const THEME_COLORS = new Set([
|
||||
"white",
|
||||
]);
|
||||
|
||||
export function computeRgbColor(color: string): string {
|
||||
export function computeCssColor(color: string): string {
|
||||
if (THEME_COLORS.has(color)) {
|
||||
return `var(--rgb-${color}-color)`;
|
||||
}
|
||||
if (color.startsWith("#")) {
|
||||
try {
|
||||
return hex2rgb(color).join(", ");
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
return `rgb(var(--rgb-${color}-color))`;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ import {
|
||||
mdiCommentAlert,
|
||||
mdiCounter,
|
||||
mdiCurrentAc,
|
||||
mdiDatabase,
|
||||
mdiEarHearing,
|
||||
mdiEye,
|
||||
mdiFan,
|
||||
mdiFlash,
|
||||
@@ -52,9 +54,12 @@ import {
|
||||
mdiScriptText,
|
||||
mdiSineWave,
|
||||
mdiSpeedometer,
|
||||
mdiSunWireless,
|
||||
mdiThermometer,
|
||||
mdiThermometerLines,
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
mdiTransmissionTower,
|
||||
mdiVideo,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
@@ -126,10 +131,13 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
apparent_power: mdiFlash,
|
||||
aqi: mdiAirFilter,
|
||||
atmospheric_pressure: mdiThermometerLines,
|
||||
// battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
|
||||
carbon_dioxide: mdiMoleculeCo2,
|
||||
carbon_monoxide: mdiMoleculeCo,
|
||||
current: mdiCurrentAc,
|
||||
data_rate: mdiTransmissionTower,
|
||||
data_size: mdiDatabase,
|
||||
date: mdiCalendar,
|
||||
distance: mdiArrowLeftRight,
|
||||
duration: mdiProgressClock,
|
||||
@@ -138,6 +146,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
gas: mdiMeterGas,
|
||||
humidity: mdiWaterPercent,
|
||||
illuminance: mdiBrightness5,
|
||||
irradiance: mdiSunWireless,
|
||||
moisture: mdiWaterPercent,
|
||||
monetary: mdiCash,
|
||||
nitrogen_dioxide: mdiMolecule,
|
||||
@@ -154,6 +163,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
pressure: mdiGauge,
|
||||
reactive_power: mdiFlash,
|
||||
signal_strength: mdiWifi,
|
||||
sound_pressure: mdiEarHearing,
|
||||
speed: mdiSpeedometer,
|
||||
sulphur_dioxide: mdiMolecule,
|
||||
temperature: mdiThermometer,
|
||||
@@ -188,6 +198,15 @@ export const DOMAINS_WITH_CARD = [
|
||||
"water_heater",
|
||||
];
|
||||
|
||||
export const SENSOR_ENTITIES = [
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
"calendar",
|
||||
"camera",
|
||||
"device_tracker",
|
||||
"weather",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
|
@@ -7,10 +7,12 @@ if (__BUILD__ === "latest" && polyfillsLoaded) {
|
||||
}
|
||||
|
||||
// Tuesday, August 10
|
||||
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateWeekdayMem(locale).format(dateObj);
|
||||
export const formatDateWeekdayDay = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatDateWeekdayDayMem(locale).format(dateObj);
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
const formatDateWeekdayDayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
@@ -92,3 +94,14 @@ const formatDateYearMem = memoizeOne(
|
||||
year: "numeric",
|
||||
})
|
||||
);
|
||||
|
||||
// Monday
|
||||
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateWeekdayMem(locale).format(dateObj);
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
})
|
||||
);
|
||||
|
@@ -39,5 +39,5 @@ export default function scrollToTarget(element, target) {
|
||||
);
|
||||
requestAnimationFrame(updateFrame.bind(element));
|
||||
}
|
||||
}.call(element));
|
||||
}).call(element);
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ export const alarmControlPanelColor = (state?: string): string | undefined => {
|
||||
return "alarm-arming";
|
||||
case "triggered":
|
||||
return "alarm-triggered";
|
||||
case "disarmed":
|
||||
return "alarm-disarmed";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
10
src/common/entity/color/alert_color.ts
Normal file
10
src/common/entity/color/alert_color.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const alertColor = (state?: string): string | undefined => {
|
||||
switch (state) {
|
||||
case "on":
|
||||
return "alert";
|
||||
case "off":
|
||||
return "alert-off";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
@@ -1,9 +1,7 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export const batteryStateColor = (stateObj: HassEntity) => {
|
||||
const value = Number(stateObj.state);
|
||||
export const batteryStateColor = (state: string) => {
|
||||
const value = Number(state);
|
||||
if (isNaN(value)) {
|
||||
return "sensor-battery-unknown";
|
||||
return undefined;
|
||||
}
|
||||
if (value >= 70) {
|
||||
return "sensor-battery-high";
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { stateActive } from "../state_active";
|
||||
|
||||
const ALERTING_DEVICE_CLASSES = new Set([
|
||||
"battery",
|
||||
"carbon_monoxide",
|
||||
"gas",
|
||||
"heat",
|
||||
"lock",
|
||||
"moisture",
|
||||
"problem",
|
||||
"safety",
|
||||
@@ -12,9 +14,15 @@ const ALERTING_DEVICE_CLASSES = new Set([
|
||||
"tamper",
|
||||
]);
|
||||
|
||||
export const binarySensorColor = (stateObj: HassEntity): string | undefined => {
|
||||
export const binarySensorColor = (
|
||||
stateObj: HassEntity,
|
||||
state: string
|
||||
): string | undefined => {
|
||||
const deviceClass = stateObj?.attributes.device_class;
|
||||
|
||||
if (!stateActive(stateObj, state)) {
|
||||
return undefined;
|
||||
}
|
||||
return deviceClass && ALERTING_DEVICE_CLASSES.has(deviceClass)
|
||||
? "binary-sensor-alerting"
|
||||
: "binary-sensor";
|
||||
|
@@ -1,3 +1,14 @@
|
||||
import { HvacAction } from "../../../data/climate";
|
||||
|
||||
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
|
||||
cooling: "var(--rgb-state-climate-cool-color)",
|
||||
drying: "var(--rgb-state-climate-dry-color)",
|
||||
fan: "var(--rgb-state-climate-fan-only-color)",
|
||||
heating: "var(--rgb-state-climate-heat-color)",
|
||||
idle: "var(--rgb-state-climate-idle-color)",
|
||||
off: "var(--rgb-state-climate-off-color)",
|
||||
};
|
||||
|
||||
export const climateColor = (state: string): string | undefined => {
|
||||
switch (state) {
|
||||
case "auto":
|
||||
|
@@ -1,5 +1,7 @@
|
||||
export const lockColor = (state?: string): string | undefined => {
|
||||
switch (state) {
|
||||
case "unlocked":
|
||||
return "lock-unlocked";
|
||||
case "locked":
|
||||
return "lock-locked";
|
||||
case "jammed":
|
||||
|
10
src/common/entity/color/person_color.ts
Normal file
10
src/common/entity/color/person_color.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const personColor = (state: string): string | undefined => {
|
||||
switch (state) {
|
||||
case "home":
|
||||
return "person-home";
|
||||
case "not_home":
|
||||
return "person-not-home";
|
||||
default:
|
||||
return "person-zone";
|
||||
}
|
||||
};
|
@@ -1,11 +1,14 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { batteryStateColor } from "./battery_color";
|
||||
|
||||
export const sensorColor = (stateObj: HassEntity): string | undefined => {
|
||||
export const sensorColor = (
|
||||
stateObj: HassEntity,
|
||||
state: string
|
||||
): string | undefined => {
|
||||
const deviceClass = stateObj?.attributes.device_class;
|
||||
|
||||
if (deviceClass === "battery") {
|
||||
return batteryStateColor(stateObj);
|
||||
return batteryStateColor(state);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
15
src/common/entity/color/update_color.ts
Normal file
15
src/common/entity/color/update_color.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UpdateEntity, updateIsInstalling } from "../../../data/update";
|
||||
import { stateActive } from "../state_active";
|
||||
|
||||
export const updateColor = (
|
||||
stateObj: HassEntity,
|
||||
state: string
|
||||
): string | undefined => {
|
||||
if (!stateActive(stateObj, state)) {
|
||||
return undefined;
|
||||
}
|
||||
return updateIsInstalling(stateObj as UpdateEntity)
|
||||
? "update-installing"
|
||||
: "update";
|
||||
};
|
52
src/common/entity/compute_attribute_display.ts
Normal file
52
src/common/entity/compute_attribute_display.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export const computeAttributeValueDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"],
|
||||
attribute: string,
|
||||
value?: any
|
||||
): string => {
|
||||
const entityId = stateObj.entity_id;
|
||||
const attributeValue =
|
||||
value !== undefined ? value : stateObj.attributes[attribute];
|
||||
const domain = computeDomain(entityId);
|
||||
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||
const translationKey = entity?.translation_key;
|
||||
|
||||
return (
|
||||
(translationKey &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}`
|
||||
)) ||
|
||||
localize(
|
||||
`component.${domain}.state_attributes._.${attribute}.state.${attributeValue}`
|
||||
) ||
|
||||
attributeValue
|
||||
);
|
||||
};
|
||||
|
||||
export const computeAttributeNameDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"],
|
||||
attribute: string
|
||||
): string => {
|
||||
const entityId = stateObj.entity_id;
|
||||
const domain = computeDomain(entityId);
|
||||
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||
const translationKey = entity?.translation_key;
|
||||
|
||||
return (
|
||||
(translationKey &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.name`
|
||||
)) ||
|
||||
localize(`component.${domain}.state_attributes._.${attribute}.name`) ||
|
||||
attribute
|
||||
);
|
||||
};
|
@@ -108,7 +108,7 @@ export const domainIconWithoutDefault = (
|
||||
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
|
||||
|
||||
case "humidifier":
|
||||
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
|
||||
|
||||
case "input_boolean":
|
||||
return compareState === "on"
|
||||
@@ -180,6 +180,9 @@ export const domainIconWithoutDefault = (
|
||||
}
|
||||
}
|
||||
|
||||
case "person":
|
||||
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
|
||||
|
||||
case "switch":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "outlet":
|
||||
|
@@ -2,7 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
|
||||
const FIXED_DOMAIN_STATES = {
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
"armed_away",
|
||||
"armed_custom_bypass",
|
||||
@@ -57,7 +57,7 @@ const FIXED_DOMAIN_STATES = {
|
||||
"windy-variant",
|
||||
"windy",
|
||||
],
|
||||
};
|
||||
} as const;
|
||||
|
||||
const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
alarm_control_panel: {
|
||||
@@ -261,6 +261,11 @@ export const getStates = (
|
||||
result.push(...state.attributes.activity_list);
|
||||
}
|
||||
break;
|
||||
case "sensor":
|
||||
if (!attribute && state.attributes.device_class === "enum") {
|
||||
result.push(...state.attributes.options);
|
||||
}
|
||||
break;
|
||||
case "vacuum":
|
||||
if (attribute === "fan_speed") {
|
||||
result.push(...state.attributes.fan_speed_list);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { OFF_STATES, UNAVAILABLE } from "../../data/entity";
|
||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
@@ -10,21 +10,32 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (OFF_STATES.includes(compareState)) {
|
||||
if (isUnavailableState(compareState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The "off" check is relevant for most domains, but there are exceptions
|
||||
// such as "alert" where "off" is still a somewhat active state and
|
||||
// therefore gets a custom color and "idle" is instead the state that
|
||||
// matches what most other domains consider inactive.
|
||||
if (compareState === OFF && domain !== "alert") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom cases
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
return compareState !== "disarmed";
|
||||
case "alert":
|
||||
// "on" and "off" are active, as "off" just means alert was acknowledged but is still active
|
||||
return compareState !== "idle";
|
||||
case "cover":
|
||||
return !["closed", "closing"].includes(compareState);
|
||||
return compareState !== "closed";
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
return compareState !== "not_home";
|
||||
case "alarm_control_panel":
|
||||
return compareState !== "disarmed";
|
||||
case "lock":
|
||||
return compareState !== "unlocked";
|
||||
return compareState !== "locked";
|
||||
case "media_player":
|
||||
return compareState !== "standby";
|
||||
case "vacuum":
|
||||
@@ -33,7 +44,11 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
return compareState === "problem";
|
||||
case "group":
|
||||
return ["on", "home", "open", "locked", "problem"].includes(compareState);
|
||||
default:
|
||||
return true;
|
||||
case "timer":
|
||||
return compareState === "active";
|
||||
case "camera":
|
||||
return compareState === "streaming";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@@ -1,79 +1,96 @@
|
||||
/** Return an color representing a state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UpdateEntity, updateIsInstalling } from "../../data/update";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { alarmControlPanelColor } from "./color/alarm_control_panel_color";
|
||||
import { alertColor } from "./color/alert_color";
|
||||
import { binarySensorColor } from "./color/binary_sensor_color";
|
||||
import { climateColor } from "./color/climate_color";
|
||||
import { lockColor } from "./color/lock_color";
|
||||
import { personColor } from "./color/person_color";
|
||||
import { sensorColor } from "./color/sensor_color";
|
||||
import { updateColor } from "./color/update_color";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stateActive } from "./state_active";
|
||||
|
||||
export const stateColorCss = (stateObj?: HassEntity, state?: string) => {
|
||||
if (!stateObj || !stateActive(stateObj, state)) {
|
||||
return `var(--rgb-disabled-color)`;
|
||||
const STATIC_ACTIVE_COLORED_DOMAIN = new Set([
|
||||
"automation",
|
||||
"calendar",
|
||||
"camera",
|
||||
"cover",
|
||||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"light",
|
||||
"media_player",
|
||||
"plant",
|
||||
"remote",
|
||||
"schedule",
|
||||
"script",
|
||||
"siren",
|
||||
"switch",
|
||||
"timer",
|
||||
"vacuum",
|
||||
]);
|
||||
|
||||
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
if (compareState === UNAVAILABLE) {
|
||||
return `var(--rgb-state-unavailable-color)`;
|
||||
}
|
||||
|
||||
const color = stateColor(stateObj, state);
|
||||
const domainColor = stateColor(stateObj, state);
|
||||
|
||||
if (color) {
|
||||
return `var(--rgb-state-${color}-color)`;
|
||||
if (domainColor) {
|
||||
return `var(--rgb-state-${domainColor}-color)`;
|
||||
}
|
||||
|
||||
return `var(--rgb-state-default-color)`;
|
||||
if (!stateActive(stateObj, state)) {
|
||||
return `var(--rgb-state-inactive-color)`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const stateColor = (stateObj: HassEntity, state?: string) => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
if (
|
||||
STATIC_ACTIVE_COLORED_DOMAIN.has(domain) &&
|
||||
stateActive(stateObj, state)
|
||||
) {
|
||||
return domain.replace("_", "-");
|
||||
}
|
||||
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
return alarmControlPanelColor(compareState);
|
||||
|
||||
case "binary_sensor":
|
||||
return binarySensorColor(stateObj);
|
||||
case "alert":
|
||||
return alertColor(compareState);
|
||||
|
||||
case "cover":
|
||||
return "cover";
|
||||
case "binary_sensor":
|
||||
return binarySensorColor(stateObj, compareState);
|
||||
|
||||
case "climate":
|
||||
return climateColor(compareState);
|
||||
|
||||
case "fan":
|
||||
return "fan";
|
||||
|
||||
case "lock":
|
||||
return lockColor(compareState);
|
||||
|
||||
case "light":
|
||||
return "light";
|
||||
|
||||
case "humidifier":
|
||||
return "humidifier";
|
||||
|
||||
case "media_player":
|
||||
return "media-player";
|
||||
case "person":
|
||||
case "device_tracker":
|
||||
return personColor(compareState);
|
||||
|
||||
case "sensor":
|
||||
return sensorColor(stateObj);
|
||||
|
||||
case "vacuum":
|
||||
return "vacuum";
|
||||
|
||||
case "siren":
|
||||
return "siren";
|
||||
return sensorColor(stateObj, compareState);
|
||||
|
||||
case "sun":
|
||||
return compareState === "above_horizon" ? "sun-day" : "sun-night";
|
||||
|
||||
case "switch":
|
||||
return "switch";
|
||||
|
||||
case "update":
|
||||
return updateIsInstalling(stateObj as UpdateEntity)
|
||||
? "update-installing"
|
||||
: "update";
|
||||
return updateColor(stateObj, compareState);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@@ -86,7 +86,7 @@ export const protocolIntegrationPicked = async (
|
||||
"ui.panel.config.integrations.config_flow.missing_zwave_zigbee",
|
||||
{
|
||||
integration: "Zigbee",
|
||||
brand: options?.brand || options?.domain || "Z-Wave",
|
||||
brand: options?.brand || options?.domain || "Zigbee",
|
||||
supported_hardware_link: html`<a
|
||||
href=${documentationUrl(
|
||||
hass,
|
||||
|
@@ -1,4 +1,15 @@
|
||||
export const stringCompare = (a: string, b: string) => {
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
const collator = memoizeOne(
|
||||
(language: string | undefined) => new Intl.Collator(language)
|
||||
);
|
||||
|
||||
const caseInsensitiveCollator = memoizeOne(
|
||||
(language: string | undefined) =>
|
||||
new Intl.Collator(language, { sensitivity: "accent" })
|
||||
);
|
||||
|
||||
const fallbackStringCompare = (a: string, b: string) => {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
@@ -9,5 +20,28 @@ export const stringCompare = (a: string, b: string) => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const caseInsensitiveStringCompare = (a: string, b: string) =>
|
||||
stringCompare(a.toLowerCase(), b.toLowerCase());
|
||||
export const stringCompare = (
|
||||
a: string,
|
||||
b: string,
|
||||
language: string | undefined = undefined
|
||||
) => {
|
||||
// @ts-ignore
|
||||
if (Intl?.Collator) {
|
||||
return collator(language).compare(a, b);
|
||||
}
|
||||
|
||||
return fallbackStringCompare(a, b);
|
||||
};
|
||||
|
||||
export const caseInsensitiveStringCompare = (
|
||||
a: string,
|
||||
b: string,
|
||||
language: string | undefined = undefined
|
||||
) => {
|
||||
// @ts-ignore
|
||||
if (Intl?.Collator) {
|
||||
return caseInsensitiveCollator(language).compare(a, b);
|
||||
}
|
||||
|
||||
return fallbackStringCompare(a.toLowerCase(), b.toLowerCase());
|
||||
};
|
||||
|
@@ -1,32 +1,10 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const iconColorCSS = css`
|
||||
ha-state-icon[data-active][data-domain="alert"],
|
||||
ha-state-icon[data-active][data-domain="automation"],
|
||||
ha-state-icon[data-active][data-domain="binary_sensor"],
|
||||
ha-state-icon[data-active][data-domain="calendar"],
|
||||
ha-state-icon[data-active][data-domain="camera"],
|
||||
ha-state-icon[data-active][data-domain="cover"],
|
||||
ha-state-icon[data-active][data-domain="device_tracker"],
|
||||
ha-state-icon[data-active][data-domain="fan"],
|
||||
ha-state-icon[data-active][data-domain="humidifier"],
|
||||
ha-state-icon[data-active][data-domain="light"],
|
||||
ha-state-icon[data-active][data-domain="input_boolean"],
|
||||
ha-state-icon[data-active][data-domain="lock"],
|
||||
ha-state-icon[data-active][data-domain="media_player"],
|
||||
ha-state-icon[data-active][data-domain="remote"],
|
||||
ha-state-icon[data-active][data-domain="script"],
|
||||
ha-state-icon[data-active][data-domain="sun"],
|
||||
ha-state-icon[data-active][data-domain="switch"],
|
||||
ha-state-icon[data-active][data-domain="timer"],
|
||||
ha-state-icon[data-active][data-domain="vacuum"],
|
||||
ha-state-icon[data-active][data-domain="group"] {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
}
|
||||
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="pending"],
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="arming"],
|
||||
ha-state-icon[data-active][data-domain="alarm_control_panel"][data-state="triggered"] {
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"],
|
||||
ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"],
|
||||
ha-state-icon[data-domain="lock"][data-state="jammed"] {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@@ -44,6 +22,6 @@ export const iconColorCSS = css`
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
ha-state-icon[data-state="unavailable"] {
|
||||
color: var(--state-unavailable-color);
|
||||
color: rgb(var(--rgb-state-unavailable-color));
|
||||
}
|
||||
`;
|
||||
|
10
src/common/translations/day_names.ts
Normal file
10
src/common/translations/day_names.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { addDays, startOfWeek } from "date-fns";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDateWeekday } from "../datetime/format_date";
|
||||
|
||||
export const dayNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
||||
Array.from({ length: 7 }, (_, d) =>
|
||||
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
|
||||
)
|
||||
);
|
@@ -12,12 +12,10 @@ import { getLocalLanguage } from "../../util/common-translation";
|
||||
export type LocalizeKeys =
|
||||
| FlattenObjectKeys<Omit<TranslationDict, "supervisor">>
|
||||
| `panel.${string}`
|
||||
| `state.${string}`
|
||||
| `state_attributes.${string}`
|
||||
| `state_badge.${string}`
|
||||
| `ui.card.alarm_control_panel.${string}`
|
||||
| `ui.card.weather.attributes.${string}`
|
||||
| `ui.card.weather.cardinal_direction.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.logbook.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
@@ -30,7 +28,6 @@ export type LocalizeKeys =
|
||||
| `ui.panel.config.dashboard.${string}`
|
||||
| `ui.panel.config.devices.${string}`
|
||||
| `ui.panel.config.energy.${string}`
|
||||
| `ui.panel.config.helpers.${string}`
|
||||
| `ui.panel.config.info.${string}`
|
||||
| `ui.panel.config.logs.${string}`
|
||||
| `ui.panel.config.lovelace.${string}`
|
||||
|
10
src/common/translations/month_names.ts
Normal file
10
src/common/translations/month_names.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { addMonths, startOfYear } from "date-fns";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDateMonth } from "../datetime/format_date";
|
||||
|
||||
export const monthNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
||||
Array.from({ length: 12 }, (_, m) =>
|
||||
formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
|
||||
)
|
||||
);
|
@@ -40,7 +40,7 @@ import {
|
||||
formatDateMonth,
|
||||
formatDateMonthYear,
|
||||
formatDateShort,
|
||||
formatDateWeekday,
|
||||
formatDateWeekdayDay,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
@@ -92,7 +92,7 @@ _adapters._date.override({
|
||||
case "hour":
|
||||
return formatTime(new Date(time), this.options.locale);
|
||||
case "weekday":
|
||||
return formatDateWeekday(new Date(time), this.options.locale);
|
||||
return formatDateWeekdayDay(new Date(time), this.options.locale);
|
||||
case "date":
|
||||
return formatDate(new Date(time), this.options.locale);
|
||||
case "day":
|
||||
|
@@ -26,7 +26,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@@ -47,7 +47,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (!this.hasUpdated || changedProps.has("showNames")) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
@@ -101,7 +101,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
propagate: true,
|
||||
},
|
||||
legend: {
|
||||
display: !this.isSingleDevice,
|
||||
display: this.showNames,
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
|
@@ -1,73 +1,14 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { rgb2hex } from "../../common/color/convert-color";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColor } from "../../common/entity/state_color";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { TimelineEntity } from "../../data/history";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
|
||||
const stateColorTokenMap: Map<string, string> = new Map();
|
||||
const stateColorMap: Map<string, string> = new Map();
|
||||
|
||||
let colorIndex = 0;
|
||||
|
||||
export const getStateColorToken = (
|
||||
stateString: string,
|
||||
entityState?: HassEntity
|
||||
) => {
|
||||
if (!entityState || !stateActive(entityState, stateString)) {
|
||||
return `disabled`;
|
||||
}
|
||||
const color = stateColor(entityState, stateString);
|
||||
if (color) {
|
||||
return `state-${color}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getColor = (
|
||||
stateString: string,
|
||||
computedStyles: CSSStyleDeclaration,
|
||||
entityState?: HassEntity
|
||||
) => {
|
||||
const stateColorToken = getStateColorToken(stateString, entityState);
|
||||
|
||||
if (stateColorToken) {
|
||||
if (stateColorTokenMap.has(stateColorToken)) {
|
||||
return stateColorTokenMap.get(stateColorToken);
|
||||
}
|
||||
const value = computedStyles.getPropertyValue(
|
||||
`--rgb-${stateColorToken}-color`
|
||||
);
|
||||
|
||||
if (value) {
|
||||
const parsedValue = value.split(",").map((v) => Number(v)) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
const hexValue = rgb2hex(parsedValue);
|
||||
stateColorTokenMap.set(stateColorToken, hexValue);
|
||||
return hexValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateColorMap.has(stateString)) {
|
||||
return stateColorMap.get(stateString);
|
||||
}
|
||||
const color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
};
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@@ -83,7 +24,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property() public identifier?: string;
|
||||
|
||||
@property({ type: Boolean }) public isSingleDevice = false;
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public chunked = false;
|
||||
|
||||
@@ -123,7 +64,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this._generateData();
|
||||
}
|
||||
|
||||
if (changedProps.has("startTime") || changedProps.has("endTime")) {
|
||||
if (
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("showNames")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
}
|
||||
@@ -175,8 +120,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
drawTicks: false,
|
||||
},
|
||||
ticks: {
|
||||
display:
|
||||
this.chunked || !this.isSingleDevice || this.data.length !== 1,
|
||||
display: this.chunked || this.showNames,
|
||||
},
|
||||
afterSetDimensions: (y) => {
|
||||
y.maxWidth = y.chart.width * 0.18;
|
||||
@@ -271,7 +215,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
start: prevLastChanged,
|
||||
end: newLastChanged,
|
||||
label: locState,
|
||||
color: getColor(
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
@@ -289,7 +233,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
start: prevLastChanged,
|
||||
end: endTime,
|
||||
label: locState,
|
||||
color: getColor(
|
||||
color: computeTimelineColor(
|
||||
prevState,
|
||||
computedStyles,
|
||||
this.hass.states[stateInfo.entity_id]
|
||||
|
@@ -48,7 +48,7 @@ class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-single" }) public noSingle = false;
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@@ -128,8 +128,7 @@ class StateHistoryCharts extends LitElement {
|
||||
.unit=${item.unit}
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.line?.length === 1}
|
||||
.showNames=${this.showNames}
|
||||
.endTime=${this._computedEndTime}
|
||||
.names=${this.names}
|
||||
></state-history-chart-line>
|
||||
@@ -141,8 +140,7 @@ class StateHistoryCharts extends LitElement {
|
||||
.data=${item}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.isSingleDevice=${!this.noSingle &&
|
||||
this.historyData.timeline?.length === 1}
|
||||
.showNames=${this.showNames}
|
||||
.names=${this.names}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
|
@@ -32,14 +32,26 @@ import {
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
|
||||
export type ExtendedStatisticType = StatisticType | "state" | "change";
|
||||
export type ExtendedStatisticType = StatisticType | "change";
|
||||
|
||||
export const supportedStatTypeMap: Record<
|
||||
ExtendedStatisticType,
|
||||
StatisticType
|
||||
> = {
|
||||
mean: "mean",
|
||||
min: "min",
|
||||
max: "max",
|
||||
sum: "sum",
|
||||
state: "sum",
|
||||
change: "sum",
|
||||
};
|
||||
|
||||
export const statTypeMap: Record<ExtendedStatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
min: "min",
|
||||
max: "max",
|
||||
sum: "sum",
|
||||
state: "sum",
|
||||
state: "state",
|
||||
change: "sum",
|
||||
};
|
||||
|
||||
|
@@ -37,8 +37,11 @@ export class TextBarElement extends BarElement {
|
||||
}
|
||||
const textColor =
|
||||
options.textColor ||
|
||||
(options.backgroundColor &&
|
||||
(luminosity(hex2rgb(options.backgroundColor)) > 0.5 ? "#000" : "#fff"));
|
||||
(options?.backgroundColor === "transparent"
|
||||
? "transparent"
|
||||
: luminosity(hex2rgb(options.backgroundColor)) > 0.5
|
||||
? "#000"
|
||||
: "#fff");
|
||||
|
||||
// ctx.font = "12px arial";
|
||||
ctx.fillStyle = textColor;
|
||||
|
102
src/components/chart/timeline-chart/timeline-color.ts
Normal file
102
src/components/chart/timeline-chart/timeline-color.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getGraphColorByIndex } from "../../../common/color/colors";
|
||||
import { lab2hex, rgb2hex, rgb2lab } from "../../../common/color/convert-color";
|
||||
import { labBrighten } from "../../../common/color/lab";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColor } from "../../../common/entity/state_color";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
|
||||
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
||||
media_player: {
|
||||
paused: 0.5,
|
||||
idle: 1,
|
||||
},
|
||||
vacuum: {
|
||||
returning: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
const cssColorMap: Map<string, [number, number, number]> = new Map();
|
||||
|
||||
function cssToRgb(
|
||||
cssVariable: string,
|
||||
computedStyles: CSSStyleDeclaration
|
||||
): [number, number, number] | undefined {
|
||||
if (!cssVariable.startsWith("--rgb")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cssColorMap.has(cssVariable)) {
|
||||
return cssColorMap.get(cssVariable)!;
|
||||
}
|
||||
|
||||
const value = computedStyles.getPropertyValue(cssVariable);
|
||||
|
||||
if (!value) return undefined;
|
||||
|
||||
const rgb = value.split(",").map((v) => Number(v)) as [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
];
|
||||
cssColorMap.set(cssVariable, rgb);
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
function computeTimelineStateColor(
|
||||
state: string,
|
||||
computedStyles: CSSStyleDeclaration,
|
||||
stateObj?: HassEntity
|
||||
): string | undefined {
|
||||
if (!stateObj || state === UNAVAILABLE) {
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
const color = stateColor(stateObj, state);
|
||||
|
||||
if (!color && !stateActive(stateObj, state)) {
|
||||
const rgb = cssToRgb("--rgb-state-inactive-color", computedStyles);
|
||||
if (!rgb) return undefined;
|
||||
return rgb2hex(rgb);
|
||||
}
|
||||
|
||||
const rgb = cssToRgb(`--rgb-state-${color}-color`, computedStyles);
|
||||
|
||||
if (!rgb) return undefined;
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
|
||||
if (!shade) {
|
||||
return rgb2hex(rgb);
|
||||
}
|
||||
return lab2hex(labBrighten(rgb2lab(rgb), shade));
|
||||
}
|
||||
|
||||
let colorIndex = 0;
|
||||
const stateColorMap: Map<string, string> = new Map();
|
||||
|
||||
function computeTimeLineGenericColor(
|
||||
state: string,
|
||||
computedStyles: CSSStyleDeclaration
|
||||
): string {
|
||||
if (stateColorMap.has(state)) {
|
||||
return stateColorMap.get(state)!;
|
||||
}
|
||||
const color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
stateColorMap.set(state, color);
|
||||
return color;
|
||||
}
|
||||
|
||||
export function computeTimelineColor(
|
||||
state: string,
|
||||
computedStyles: CSSStyleDeclaration,
|
||||
stateObj?: HassEntity
|
||||
): string {
|
||||
return (
|
||||
computeTimelineStateColor(state, computedStyles, stateObj) ||
|
||||
computeTimeLineGenericColor(state, computedStyles)
|
||||
);
|
||||
}
|
@@ -266,14 +266,16 @@ export const getCountryOptions = memoizeOne((language?: string) => {
|
||||
value: country,
|
||||
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
||||
}));
|
||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||
);
|
||||
return options;
|
||||
});
|
||||
|
||||
export const createCountryListEl = () => {
|
||||
export const createCountryListEl = (language?: string) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "countries";
|
||||
const options = getCountryOptions();
|
||||
const options = getCountryOptions(language);
|
||||
for (const country of options) {
|
||||
const option = document.createElement("option");
|
||||
option.value = country.value;
|
||||
|
@@ -157,7 +157,7 @@ export const CURRENCIES = [
|
||||
"XPF",
|
||||
"YER",
|
||||
"ZAR",
|
||||
"ZMK",
|
||||
"ZMW",
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
@@ -173,14 +173,16 @@ export const getCurrencyOptions = memoizeOne((language?: string) => {
|
||||
value: currency,
|
||||
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
|
||||
}));
|
||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||
);
|
||||
return options;
|
||||
});
|
||||
|
||||
export const createCurrencyListEl = () => {
|
||||
export const createCurrencyListEl = (language: string) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of getCurrencyOptions()) {
|
||||
for (const currency of getCurrencyOptions(language)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency.value;
|
||||
option.innerText = currency.label;
|
||||
|
@@ -627,7 +627,9 @@ export class HaDataTable extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__row.clickable:not(.mdc-data-table__row--selected):hover {
|
||||
.mdc-data-table__row.clickable:not(
|
||||
.mdc-data-table__row--selected
|
||||
):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
|
||||
@@ -734,13 +736,17 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
|
||||
.not-sorted
|
||||
) {
|
||||
text-align: left;
|
||||
}
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:hover,
|
||||
:host([dir="rtl"])
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(.not-sorted) {
|
||||
.mdc-data-table__header-cell.sortable.mdc-data-table__header-cell--icon:not(
|
||||
.not-sorted
|
||||
) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import DateRangePicker from "vue2-daterange-picker";
|
||||
// @ts-ignore
|
||||
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { Constructor } from "../types";
|
||||
|
||||
const Component = Vue.extend({
|
||||
props: {
|
||||
@@ -47,35 +46,26 @@ const Component = Vue.extend({
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
return createElement(DateRangePicker, {
|
||||
props: {
|
||||
// @ts-ignore
|
||||
"time-picker": this.timePicker,
|
||||
// @ts-ignore
|
||||
"auto-apply": this.autoApply,
|
||||
opens: "right",
|
||||
"show-dropdowns": false,
|
||||
// @ts-ignore
|
||||
"time-picker24-hour": this.twentyfourHours,
|
||||
// @ts-ignore
|
||||
disabled: this.disabled,
|
||||
// @ts-ignore
|
||||
ranges: this.ranges ? {} : false,
|
||||
"locale-data": {
|
||||
// @ts-ignore
|
||||
firstDay: this.firstDay,
|
||||
},
|
||||
},
|
||||
model: {
|
||||
value: {
|
||||
// @ts-ignore
|
||||
startDate: this.startDate,
|
||||
// @ts-ignore
|
||||
endDate: this.endDate,
|
||||
},
|
||||
callback: (value) => {
|
||||
// @ts-ignore
|
||||
fireEvent(this.$el as HTMLElement, "change", value);
|
||||
},
|
||||
expression: "dateRange",
|
||||
@@ -106,7 +96,11 @@ const Component = Vue.extend({
|
||||
},
|
||||
});
|
||||
|
||||
const WrappedElement: Constructor<HTMLElement> = wrap(Vue, Component);
|
||||
// Assertion corrects HTMLElement type from package
|
||||
const WrappedElement = wrap(
|
||||
Vue,
|
||||
Component
|
||||
) as unknown as CustomElementConstructor;
|
||||
|
||||
@customElement("date-range-picker")
|
||||
class DateRangePickerElement extends WrappedElement {
|
||||
|
@@ -189,7 +189,8 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
devicesByArea[a].name || "",
|
||||
devicesByArea[b].name || ""
|
||||
devicesByArea[b].name || "",
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
.map((key) => devicesByArea[key]);
|
||||
|
@@ -84,6 +84,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of devices to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-devices
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-devices" })
|
||||
public excludeDevices?: string[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
@@ -104,7 +112,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"]
|
||||
deviceFilter: this["deviceFilter"],
|
||||
excludeDevices: this["excludeDevices"]
|
||||
): Device[] => {
|
||||
if (!devices.length) {
|
||||
return [
|
||||
@@ -164,6 +173,12 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeDevices) {
|
||||
inputDevices = inputDevices.filter(
|
||||
(device) => !excludeDevices!.includes(device.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
@@ -216,7 +231,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
return outputDevices;
|
||||
}
|
||||
return outputDevices.sort((a, b) =>
|
||||
stringCompare(a.name || "", b.name || "")
|
||||
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -258,7 +273,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter
|
||||
this.deviceFilter,
|
||||
this.excludeDevices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
||||
@@ -95,7 +96,10 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.entityFilter=${this._getEntityFilter(
|
||||
this.value,
|
||||
this.entityFilter
|
||||
)}
|
||||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
@@ -114,7 +118,7 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
.excludeEntities=${this.excludeEntities}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
|
||||
.entityFilter=${this._entityFilter}
|
||||
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
|
||||
.label=${this.pickEntityLabel}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -125,11 +129,15 @@ class HaEntitiesPickerLight extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityFilter: HaEntityPickerEntityFilterFunc = (
|
||||
stateObj: HassEntity
|
||||
) =>
|
||||
(!this.value || !this.value.includes(stateObj.entity_id)) &&
|
||||
(!this.entityFilter || this.entityFilter(stateObj));
|
||||
private _getEntityFilter = memoizeOne(
|
||||
(
|
||||
value: string[] | undefined,
|
||||
entityFilter: HaEntityPickerEntityFilterFunc | undefined
|
||||
): HaEntityPickerEntityFilterFunc =>
|
||||
(stateObj: HassEntity) =>
|
||||
(!value || !value.includes(stateObj.entity_id)) &&
|
||||
(!entityFilter || entityFilter(stateObj))
|
||||
);
|
||||
|
||||
private get _currentEntities() {
|
||||
return this.value || [];
|
||||
|
@@ -174,7 +174,8 @@ export class HaEntityPicker extends LitElement {
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
entityB.friendly_name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -205,7 +206,8 @@ export class HaEntityPicker extends LitElement {
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
entityB.friendly_name
|
||||
entityB.friendly_name,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
@@ -22,7 +22,7 @@ import "../ha-switch";
|
||||
const isOn = (stateObj?: HassEntity) =>
|
||||
stateObj !== undefined &&
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!UNAVAILABLE_STATES.includes(stateObj.state);
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
|
@@ -10,21 +10,46 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import "../ha-state-icon";
|
||||
|
||||
// Define the domains whose states have special truncated strings
|
||||
const TRUNCATED_DOMAINS = [
|
||||
"alarm_control_panel",
|
||||
"device_tracker",
|
||||
"person",
|
||||
] as const satisfies ReadonlyArray<keyof typeof FIXED_DOMAIN_STATES>;
|
||||
|
||||
type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
|
||||
type TruncatedKey = {
|
||||
[T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
|
||||
}[TruncatedDomain];
|
||||
|
||||
const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||
if (
|
||||
arrayLiteralIncludes(TRUNCATED_DOMAINS)(domainKey) &&
|
||||
arrayLiteralIncludes(FIXED_DOMAIN_STATES[domainKey])(stateKey)
|
||||
) {
|
||||
return `${domainKey}.${stateKey}` as TruncatedKey;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -79,8 +104,10 @@ export class HaStateLabelBadge extends LitElement {
|
||||
// 4. Icon determined via entity state
|
||||
// 5. Value string as fallback
|
||||
const domain = computeStateDomain(entityState);
|
||||
const entry = this.hass?.entities[entityState.entity_id];
|
||||
|
||||
const showIcon = this.icon || this._computeShowIcon(domain, entityState);
|
||||
const showIcon =
|
||||
this.icon || this._computeShowIcon(domain, entityState, entry);
|
||||
const image = this.icon
|
||||
? ""
|
||||
: this.image
|
||||
@@ -88,7 +115,9 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: entityState.attributes.entity_picture_local ||
|
||||
entityState.attributes.entity_picture;
|
||||
const value =
|
||||
!image && !showIcon ? this._computeValue(domain, entityState) : undefined;
|
||||
!image && !showIcon
|
||||
? this._computeValue(domain, entityState, entry)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-label-badge
|
||||
@@ -128,7 +157,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeValue(domain: string, entityState: HassEntity) {
|
||||
private _computeValue(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
entry?: EntityRegistryEntry
|
||||
) {
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
case "binary_sensor":
|
||||
@@ -141,7 +174,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return null;
|
||||
// @ts-expect-error we don't break and go to default
|
||||
case "sensor":
|
||||
if (entityState.attributes.device_class === "moon__phase") {
|
||||
if (entry?.platform === "moon") {
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
@@ -164,7 +197,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _computeShowIcon(domain: string, entityState: HassEntity): boolean {
|
||||
private _computeShowIcon(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
entry?: EntityRegistryEntry
|
||||
): boolean {
|
||||
if (entityState.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
@@ -180,25 +217,28 @@ export class HaStateLabelBadge extends LitElement {
|
||||
case "timer":
|
||||
return true;
|
||||
case "sensor":
|
||||
return entityState.attributes.device_class === "moon__phase";
|
||||
return entry?.platform === "moon";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabel(domain, entityState, _timerTimeRemaining) {
|
||||
if (
|
||||
entityState.state === UNAVAILABLE ||
|
||||
["device_tracker", "alarm_control_panel", "person"].includes(domain)
|
||||
) {
|
||||
// Localize the state with a special state_badge namespace, which has variations of
|
||||
// the state translations that are truncated to fit within the badge label. Translations
|
||||
// are only added for device_tracker, alarm_control_panel and person.
|
||||
return (
|
||||
this.hass!.localize(`state_badge.${domain}.${entityState.state}`) ||
|
||||
this.hass!.localize(`state_badge.default.${entityState.state}`) ||
|
||||
entityState.state
|
||||
);
|
||||
private _computeLabel(
|
||||
domain: string,
|
||||
entityState: HassEntity,
|
||||
_timerTimeRemaining = 0
|
||||
) {
|
||||
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||
if (isUnavailableState(entityState.state)) {
|
||||
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||
}
|
||||
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||
if (domainStateKey) {
|
||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||
}
|
||||
// Person and device tracker state can be zone name
|
||||
if (domain === "person" || domain === "device_tracker") {
|
||||
return entityState.state;
|
||||
}
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
|
@@ -3,7 +3,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/ensure-array";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
@@ -177,7 +177,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (output.length > 1) {
|
||||
output.sort((a, b) => stringCompare(a.name || "", b.name || ""));
|
||||
output.sort((a, b) =>
|
||||
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
output.push({
|
||||
|
@@ -11,10 +11,11 @@ import {
|
||||
import { property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { CLIMATE_HVAC_ACTION_COLORS } from "../../common/entity/color/climate_color";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import { stateColor } from "../../common/entity/state_color";
|
||||
import { stateColorCss } from "../../common/entity/state_color";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -31,10 +32,12 @@ export class StateBadge extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public stateColor?: boolean;
|
||||
|
||||
@property() public color?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "icon" })
|
||||
private _showIcon = true;
|
||||
|
||||
@state() private _iconStyle: { [name: string]: string } = {};
|
||||
@state() private _iconStyle: { [name: string]: string | undefined } = {};
|
||||
|
||||
private get _stateColor() {
|
||||
const domain = this.stateObj
|
||||
@@ -58,11 +61,9 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
const active = this._stateColor && stateObj ? stateActive(stateObj) : false;
|
||||
|
||||
return html`<ha-state-icon
|
||||
style=${styleMap(this._iconStyle)}
|
||||
?data-active=${active}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
.icon=${this.overrideIcon}
|
||||
@@ -76,7 +77,8 @@ export class StateBadge extends LitElement {
|
||||
!changedProps.has("stateObj") &&
|
||||
!changedProps.has("overrideImage") &&
|
||||
!changedProps.has("overrideIcon") &&
|
||||
!changedProps.has("stateColor")
|
||||
!changedProps.has("stateColor") &&
|
||||
!changedProps.has("color")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -107,12 +109,16 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
hostStyle.backgroundImage = `url(${imageUrl})`;
|
||||
this._showIcon = false;
|
||||
} else if (stateActive(stateObj) && this._stateColor) {
|
||||
const iconColor = stateColor(stateObj);
|
||||
} else if (this.color) {
|
||||
// Externally provided overriding color wins over state color
|
||||
iconStyle.color = this.color;
|
||||
} else if (this._stateColor && stateActive(stateObj)) {
|
||||
const color = stateColorCss(stateObj);
|
||||
if (color) {
|
||||
iconStyle.color = `rgb(${color})`;
|
||||
}
|
||||
if (stateObj.attributes.rgb_color) {
|
||||
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
||||
} else if (iconColor) {
|
||||
iconStyle.color = `rgb(var(--rgb-state-${iconColor}-color))`;
|
||||
}
|
||||
if (stateObj.attributes.brightness) {
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
@@ -126,6 +132,14 @@ export class StateBadge extends LitElement {
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
if (stateObj.attributes.hvac_action) {
|
||||
const hvacAction = stateObj.attributes.hvac_action;
|
||||
if (["heating", "cooling", "drying"].includes(hvacAction)) {
|
||||
iconStyle.color = `rgb(${CLIMATE_HVAC_ACTION_COLORS[hvacAction]})`;
|
||||
} else {
|
||||
delete iconStyle.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.overrideImage) {
|
||||
let imageUrl = this.overrideImage;
|
||||
|
@@ -19,6 +19,8 @@ class StateInfo extends LitElement {
|
||||
// property used only in CSS
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
@property() public color?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return html``;
|
||||
@@ -29,6 +31,7 @@ class StateInfo extends LitElement {
|
||||
return html`<state-badge
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
></state-badge>
|
||||
<div class="info">
|
||||
<div class="name" .title=${name} .inDialog=${this.inDialog}>
|
||||
|
@@ -16,7 +16,11 @@ const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary">${item.slug}</span>
|
||||
${item.icon
|
||||
? html`<img slot="graphic" .src="/api/hassio/addons/${item.slug}/icon" />`
|
||||
? html`<img
|
||||
alt=""
|
||||
slot="graphic"
|
||||
.src="/api/hassio/addons/${item.slug}/icon"
|
||||
/>`
|
||||
: ""}
|
||||
</mwc-list-item>`;
|
||||
|
||||
@@ -80,7 +84,9 @@ class HaAddonPicker extends LitElement {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._addons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
.sort((a, b) => stringCompare(a.name, b.name));
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
} else {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
|
@@ -73,6 +73,14 @@ export class HaAreaPicker extends LitElement {
|
||||
@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[];
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
@@ -109,7 +117,8 @@ export class HaAreaPicker extends LitElement {
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"]
|
||||
noAdd: this["noAdd"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): AreaRegistryEntry[] => {
|
||||
if (!areas.length) {
|
||||
return [
|
||||
@@ -235,6 +244,12 @@ export class HaAreaPicker extends LitElement {
|
||||
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!outputAreas.length) {
|
||||
outputAreas = [
|
||||
{
|
||||
@@ -264,7 +279,7 @@ export class HaAreaPicker extends LitElement {
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this.comboBox as any).items = this._getAreas(
|
||||
const areas = this._getAreas(
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
@@ -273,8 +288,11 @@ export class HaAreaPicker extends LitElement {
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
this.noAdd,
|
||||
this.excludeAreas
|
||||
);
|
||||
(this.comboBox as any).items = areas;
|
||||
(this.comboBox as any).filteredItems = areas;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +402,8 @@ export class HaAreaPicker extends LitElement {
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd
|
||||
this.noAdd,
|
||||
this.excludeAreas
|
||||
);
|
||||
await this.updateComplete;
|
||||
await this.comboBox.updateComplete;
|
||||
|
@@ -272,11 +272,17 @@ export class HaBarSlider extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
--slider-bar-color: rgb(var(--rgb-primary-color));
|
||||
--slider-bar-background: rgba(var(--rgb-disabled-color), 0.2);
|
||||
--slider-bar-background: rgb(var(--rgb-disabled-color));
|
||||
--slider-bar-background-opacity: 0.2;
|
||||
--slider-bar-thickness: 40px;
|
||||
--slider-bar-border-radius: 10px;
|
||||
height: var(--slider-bar-thickness);
|
||||
width: 100%;
|
||||
border-radius: var(--slider-bar-border-radius);
|
||||
outline: none;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--slider-bar-color);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--slider-bar-thickness);
|
||||
@@ -301,6 +307,7 @@ export class HaBarSlider extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: var(--slider-bar-background);
|
||||
opacity: var(--slider-bar-background-opacity);
|
||||
}
|
||||
.slider .slider-track-bar {
|
||||
--border-radius: var(--slider-bar-border-radius);
|
||||
|
@@ -74,6 +74,7 @@ export class HaBarSwitch extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="switch">
|
||||
<div class="background"></div>
|
||||
<div class="button" aria-hidden="true">
|
||||
${this.checked
|
||||
? this.pathOn
|
||||
@@ -91,8 +92,9 @@ export class HaBarSwitch extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--switch-bar-color-on: var(--rgb-primary-color);
|
||||
--switch-bar-color-off: var(--rgb-disabled-color);
|
||||
--switch-bar-on-color: rgb(var(--rgb-primary-color));
|
||||
--switch-bar-off-color: rgb(var(--rgb-disabled-color));
|
||||
--switch-bar-background-opacity: 0.2;
|
||||
--switch-bar-thickness: 40px;
|
||||
--switch-bar-border-radius: 12px;
|
||||
--switch-bar-padding: 4px;
|
||||
@@ -102,6 +104,14 @@ export class HaBarSwitch extends LitElement {
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
outline: none;
|
||||
}
|
||||
:host(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--switch-bar-off-color);
|
||||
}
|
||||
:host([checked]:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--switch-bar-on-color);
|
||||
}
|
||||
.switch {
|
||||
box-sizing: border-box;
|
||||
@@ -109,11 +119,20 @@ export class HaBarSwitch extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--switch-bar-border-radius);
|
||||
background-color: rgba(var(--switch-bar-color-off), 0.3);
|
||||
overflow: hidden;
|
||||
padding: var(--switch-bar-padding);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
display: flex;
|
||||
}
|
||||
.switch .background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--switch-bar-off-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: var(--switch-bar-background-opacity);
|
||||
}
|
||||
.switch .button {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
@@ -123,18 +142,18 @@ export class HaBarSwitch extends LitElement {
|
||||
);
|
||||
transition: transform 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
background-color: rgb(var(--switch-bar-color-off));
|
||||
background-color: var(--switch-bar-off-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
:host([checked]) .switch {
|
||||
background-color: rgba(var(--switch-bar-color-on), 0.3);
|
||||
:host([checked]) .switch .background {
|
||||
background-color: var(--switch-bar-on-color);
|
||||
}
|
||||
:host([checked]) .switch .button {
|
||||
transform: translateX(100%);
|
||||
background-color: rgb(var(--switch-bar-color-on));
|
||||
background-color: var(--switch-bar-on-color);
|
||||
}
|
||||
:host([reversed]) .switch {
|
||||
flex-direction: row-reverse;
|
||||
|
@@ -266,6 +266,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
seconds: this.seconds,
|
||||
milliseconds: this.milliseconds,
|
||||
};
|
||||
if (this.enableDay) {
|
||||
value.days = this.days;
|
||||
}
|
||||
if (this.format === 12) {
|
||||
value.amPm = this.amPm;
|
||||
}
|
||||
|
@@ -46,7 +46,9 @@ class HaBluePrintPicker extends LitElement {
|
||||
...(blueprint as Blueprint).metadata,
|
||||
path,
|
||||
}));
|
||||
return result.sort((a, b) => stringCompare(a.name, b.name));
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass!.locale.language)
|
||||
);
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user