mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-29 12:31:52 +00:00
Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b36ec314 | |||
| 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 | |||
| 3dfd401036 | |||
| 71a5d8c6f9 | |||
| a0bf582cc9 | |||
| 77a53ffc6c | |||
| b6f1d78b7f | |||
| 1fda303b23 | |||
| 241645fe8d | |||
| faa57e4c02 | |||
| 915563ce6c | |||
| 44502d2c8d | |||
| b97a9ef311 | |||
| 1a66b8a374 | |||
| 4190ff5a2b | |||
| dfc461ce05 | |||
| dff7f653b1 | |||
| eccc6a8cdb | |||
| 52235c6187 | |||
| 594b402bd5 | |||
| 684f6db4df | |||
| 883e5e3a6c | |||
| 29452841c2 | |||
| 9b6e33cfec | |||
| e43f3b193e | |||
| 40d0455936 | |||
| 90a7c2d2ff | |||
| d4cda0c106 | |||
| c92e6423e8 |
@@ -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,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
|
||||
|
||||
@@ -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.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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 }}
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
@@ -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.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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 }}
|
||||
@@ -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.2.0
|
||||
|
||||
- name: Set up Node ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v3.5.1
|
||||
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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.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.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
@@ -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.2.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.5.1
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: yarn
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v3.2.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
+9
-9
@@ -2,20 +2,20 @@
|
||||
.reify-cache
|
||||
|
||||
# build
|
||||
build
|
||||
hass_frontend/*
|
||||
dist
|
||||
translations
|
||||
build/
|
||||
dist/
|
||||
/hass_frontend/
|
||||
/translations/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
.yarn/**
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
node_modules/*
|
||||
/node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
@@ -27,11 +27,11 @@ npm-debug.log
|
||||
# venv stuff
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv/*
|
||||
/venv/
|
||||
.venv
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
.vscode/**
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
@@ -46,4 +46,4 @@ src/cast/dev_const.ts
|
||||
.tool-versions
|
||||
|
||||
# Home Assistant config
|
||||
/config
|
||||
/config/
|
||||
|
||||
Vendored
+3
-2
@@ -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,5 +1,5 @@
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const fs = require("fs/promises");
|
||||
const mapStream = require("map-stream");
|
||||
|
||||
const inDirFrontend = "translations/frontend";
|
||||
@@ -46,18 +46,21 @@ gulp.task("check-translations-html", function () {
|
||||
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||
});
|
||||
|
||||
gulp.task("check-all-files-exist", function () {
|
||||
const file = fs.readFileSync(srcMeta, { encoding });
|
||||
gulp.task("check-all-files-exist", async function () {
|
||||
const file = await fs.readFile(srcMeta, { encoding });
|
||||
const meta = JSON.parse(file);
|
||||
const writings = [];
|
||||
Object.keys(meta).forEach((lang) => {
|
||||
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
|
||||
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
|
||||
}
|
||||
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
|
||||
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
|
||||
}
|
||||
writings.push(
|
||||
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
|
||||
flag: "wx",
|
||||
}),
|
||||
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
|
||||
flag: "wx",
|
||||
})
|
||||
);
|
||||
});
|
||||
return Promise.resolve();
|
||||
await Promise.allSettled(writings);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
|
||||
@@ -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>`,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Bar Sliders
|
||||
title: Bar Slider
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ import "../../../../src/components/ha-card";
|
||||
const sliders: {
|
||||
id: string;
|
||||
label: string;
|
||||
mode?: "start" | "end" | "indicator";
|
||||
mode?: "start" | "end" | "cursor";
|
||||
class?: string;
|
||||
}[] = [
|
||||
{
|
||||
@@ -22,9 +22,9 @@ const sliders: {
|
||||
mode: "end",
|
||||
},
|
||||
{
|
||||
id: "slider-indicator",
|
||||
label: "Slider (indicator mode)",
|
||||
mode: "indicator",
|
||||
id: "slider-cursor",
|
||||
label: "Slider (cursor mode)",
|
||||
mode: "cursor",
|
||||
},
|
||||
{
|
||||
id: "slider-start-custom",
|
||||
@@ -39,9 +39,9 @@ const sliders: {
|
||||
class: "custom",
|
||||
},
|
||||
{
|
||||
id: "slider-indicator-custom",
|
||||
label: "Slider (indicator mode) and custom style",
|
||||
mode: "indicator",
|
||||
id: "slider-cursor-custom",
|
||||
label: "Slider (cursor mode) and custom style",
|
||||
mode: "cursor",
|
||||
class: "custom",
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Bar Switches
|
||||
title: Bar Switch
|
||||
---
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
---
|
||||
title: Chips
|
||||
title: Chip
|
||||
---
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Dialogs
|
||||
title: Dialog
|
||||
subtitle: Dialogs provide important prompts in a user flow.
|
||||
---
|
||||
|
||||
# Material Design 3
|
||||
|
||||
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on it's [website](https://m3.material.io/components/dialogs/overview).
|
||||
Our dialogs are based on the latest version of Material Design. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
|
||||
|
||||
# Highlighted guidelines
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Gauge
|
||||
---
|
||||
|
||||
<style>
|
||||
ha-gauge {
|
||||
display: block;
|
||||
width: 200px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Gauge `<ha-gauge>`
|
||||
|
||||
A gauge that can be used to represent sensor data and provide visual feedback about the value and the corresponding severity (success, warning, error).
|
||||
|
||||
## Examples
|
||||
|
||||
Info color gauge
|
||||
<ha-gauge value="75" style="--gauge-color: var(--info-color)"></ha-gauge>
|
||||
|
||||
Success color gauge
|
||||
<ha-gauge value="25" style="--gauge-color: var(--success-color)" label="°C"></ha-gauge>
|
||||
|
||||
Warning color gauge
|
||||
<ha-gauge value="50" style="--gauge-color: var(--warning-color)" label="°C"></ha-gauge>
|
||||
|
||||
Error color gauge
|
||||
<ha-gauge value="75" style="--gauge-color: var(--error-color)" label="°C"></ha-gauge>
|
||||
|
||||
Gauge with background color
|
||||
<ha-gauge value="75" style="--gauge-color: var(--info-color); --primary-background-color: lightgray"></ha-gauge>
|
||||
|
||||
|
||||
## CSS variables
|
||||
|
||||
### Gauge
|
||||
|
||||
`primary-background-color`
|
||||
Background color of the dial (rounded arch)
|
||||
|
||||
`primary-text-color`
|
||||
Text color below dial (value and unit of measurement) plus needle color (if gauge is in needle mode)
|
||||
|
||||
#### Dial colors
|
||||
|
||||
`gauge-color`
|
||||
Used in the coding to control what color the gauge value is rendered with, but cannot be set via theme since its value will dynamically be set (either to `info-color` or to the matching severity variable if the severity color mode is used). To control the used colors, adjust the following variables.
|
||||
|
||||
`success-color`
|
||||
Dial color for the "green" severity level
|
||||
|
||||
`warning-color`
|
||||
Dial color for the "yellow" severity level
|
||||
|
||||
`error-color`
|
||||
Dial color for the "red" severity level
|
||||
|
||||
`info-color`
|
||||
Static dial color if not in severity color mode
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-gauge";
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Selectors
|
||||
title: Selector
|
||||
---
|
||||
|
||||
See the website for [list of available selectors](https://www.home-assistant.io/docs/blueprint/selectors/).
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: Switch / Toggle
|
||||
---
|
||||
|
||||
<style>
|
||||
ha-switch {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
# Switch `<ha-switch>`
|
||||
|
||||
A toggle switch can represent two states: on and off.
|
||||
|
||||
## Examples
|
||||
|
||||
Switch in on state
|
||||
<ha-switch checked></ha-switch>
|
||||
|
||||
Switch in off state
|
||||
<ha-switch></ha-switch>
|
||||
|
||||
Disabled switch
|
||||
<ha-switch disabled></ha-switch>
|
||||
|
||||
## CSS variables
|
||||
|
||||
For the switch / toggle there are always two variables, one for the on / checked state and one for the off / unchecked state.
|
||||
|
||||
The track element (background rounded rectangle that the round circular handle travels on) is set to being half transparent, so the final color will also be impacted by the color behind the track.
|
||||
|
||||
`switch-checked-color` / `switch-unchecked-color`
|
||||
Set both the color of the round handle and the track behind it. If you want to control them separately, use the variables below instead.
|
||||
|
||||
`switch-checked-button-color` / `switch-unchecked-button-color`
|
||||
Color of the round handle
|
||||
|
||||
`switch-checked-track-color` / `switch-unchecked-track-color`
|
||||
Color of the track behind the round handle
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-switch";
|
||||
@@ -1,3 +1,3 @@
|
||||
---
|
||||
title: Tips
|
||||
title: Tip
|
||||
---
|
||||
|
||||
@@ -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
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,13 +23,12 @@ const CONFIGS = [
|
||||
heading: "Basic example",
|
||||
config: `
|
||||
- type: gauge
|
||||
title: Humidity
|
||||
entity: sensor.outside_humidity
|
||||
name: Outside Humidity
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Custom Unit of Measurement",
|
||||
heading: "Custom unit of measurement",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.outside_temperature
|
||||
@@ -38,7 +37,16 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Rendering needle",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.outside_humidity
|
||||
name: Outside Humidity
|
||||
needle: true
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness
|
||||
@@ -50,7 +58,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_medium
|
||||
@@ -62,7 +70,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Severity Levels",
|
||||
heading: "Setting severity levels",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_high
|
||||
@@ -74,7 +82,7 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Setting Min (0) and Max (15) Values",
|
||||
heading: "Setting min (0) and mx (15) values",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness
|
||||
@@ -84,14 +92,14 @@ const CONFIGS = [
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Invalid Entity",
|
||||
heading: "Invalid entity",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.invalid_entity
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Non-Numeric Value",
|
||||
heading: "Non-numeric value",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: plant.bonsai
|
||||
|
||||
@@ -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,
|
||||
@@ -307,7 +341,8 @@ export class DemoEntityState extends LitElement {
|
||||
html`${computeStateDisplay(
|
||||
hass.localize,
|
||||
entry.stateObj,
|
||||
hass.locale
|
||||
hass.locale,
|
||||
hass.entities
|
||||
)}`,
|
||||
},
|
||||
device_class: {
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
+8
-7
@@ -24,7 +24,7 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"@codemirror/autocomplete": "^0.19.12",
|
||||
"@codemirror/commands": "^0.19.8",
|
||||
"@codemirror/gutter": "^0.19.9",
|
||||
@@ -100,12 +100,13 @@
|
||||
"@vue/web-component-wrapper": "^1.2.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",
|
||||
@@ -129,6 +130,7 @@
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"rrule": "^2.7.1",
|
||||
"sortablejs": "^1.14.0",
|
||||
"superstruct": "^0.15.2",
|
||||
"tinykeys": "^1.1.3",
|
||||
@@ -182,7 +184,7 @@
|
||||
"@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",
|
||||
@@ -192,15 +194,14 @@
|
||||
"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",
|
||||
"fancy-log": "^2.0.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"glob": "^7.2.0",
|
||||
"gulp": "^4.0.2",
|
||||
@@ -223,7 +224,7 @@
|
||||
"object-hash": "^2.0.3",
|
||||
"open": "^7.0.4",
|
||||
"pinst": "^3.0.0",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier": "^2.8.1",
|
||||
"require-dir": "^1.2.0",
|
||||
"rollup": "^2.8.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20221108.0"
|
||||
version = "20230102.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Install node modules
|
||||
yarn install
|
||||
yarn install
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Setup translation fetching during development
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
./node_modules/.bin/gulp setup-and-fetch-nightly-translations
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,14 @@ export const DOMAINS_WITH_CARD = [
|
||||
"water_heater",
|
||||
];
|
||||
|
||||
export const SENSOR_ENTITIES = [
|
||||
"sensor",
|
||||
"binary_sensor",
|
||||
"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",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,19 +1,28 @@
|
||||
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",
|
||||
"smoke",
|
||||
"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":
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstallingFromAttributes,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
} from "../../data/update";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
@@ -23,11 +25,13 @@ export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): string =>
|
||||
computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
entities,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
@@ -36,6 +40,7 @@ export const computeStateDisplay = (
|
||||
export const computeStateDisplayFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
entities: HomeAssistant["entities"],
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
@@ -194,7 +199,13 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
|
||||
@@ -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,94 @@
|
||||
/** 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",
|
||||
"remote",
|
||||
"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));
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export const countries = [
|
||||
import memoizeOne from "memoize-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
|
||||
export const COUNTRIES = [
|
||||
"AD",
|
||||
"AE",
|
||||
"AF",
|
||||
@@ -250,23 +253,33 @@ export const countries = [
|
||||
"ZW",
|
||||
];
|
||||
|
||||
export const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
export const getCountryOptions = memoizeOne((language?: string) => {
|
||||
const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
export const createCountryListEl = () => {
|
||||
const options = COUNTRIES.map((country) => ({
|
||||
value: country,
|
||||
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
||||
}));
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, language)
|
||||
);
|
||||
return options;
|
||||
});
|
||||
|
||||
export const createCountryListEl = (language?: string) => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "countries";
|
||||
for (const country of countries) {
|
||||
const options = getCountryOptions(language);
|
||||
for (const country of options) {
|
||||
const option = document.createElement("option");
|
||||
option.value = country;
|
||||
option.innerText = countryDisplayNames
|
||||
? countryDisplayNames.of(country)!
|
||||
: country;
|
||||
option.value = country.value;
|
||||
option.innerText = country.label;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const currencies = [
|
||||
import memoizeOne from "memoize-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
|
||||
export const CURRENCIES = [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
@@ -158,23 +161,31 @@ export const currencies = [
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
export const currencyDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "currency",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
export const getCurrencyOptions = memoizeOne((language?: string) => {
|
||||
const currencyDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "currency",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
const options = CURRENCIES.map((currency) => ({
|
||||
value: currency,
|
||||
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
|
||||
}));
|
||||
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 currencies) {
|
||||
for (const currency of getCurrencyOptions(language)) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerText = currencyDisplayNames
|
||||
? currencyDisplayNames.of(currency)!
|
||||
: currency;
|
||||
option.value = currency.value;
|
||||
option.innerText = currency.label;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ import { Constructor } from "../types";
|
||||
|
||||
const Component = Vue.extend({
|
||||
props: {
|
||||
timePicker: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
twentyfourHours: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@@ -37,13 +41,19 @@ const Component = Vue.extend({
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
autoApply: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-ignore
|
||||
return createElement(DateRangePicker, {
|
||||
props: {
|
||||
"time-picker": true,
|
||||
"auto-apply": false,
|
||||
// @ts-ignore
|
||||
"time-picker": this.timePicker,
|
||||
// @ts-ignore
|
||||
"auto-apply": this.autoApply,
|
||||
opens: "right",
|
||||
"show-dropdowns": false,
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
@@ -45,7 +34,7 @@ const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaAreaDevicesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -82,25 +71,22 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _areaPicker = true;
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
private _selectedDevices: string[] = [];
|
||||
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getAreasWithDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
deviceReg: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
entityReg: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): AreaDevices[] => {
|
||||
const devices = Object.values(deviceReg);
|
||||
const entities = Object.values(entityReg);
|
||||
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -164,11 +150,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
this._filteredDevices = inputDevices;
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const devicesByArea: DevicesByArea = {};
|
||||
|
||||
for (const device of inputDevices) {
|
||||
@@ -177,7 +158,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
if (!(areaId in devicesByArea)) {
|
||||
devicesByArea[areaId] = {
|
||||
id: areaId,
|
||||
name: areaLookup[areaId].name,
|
||||
name: areas[areaId].name,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
@@ -189,7 +170,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]);
|
||||
@@ -198,20 +180,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("area") && this.area) {
|
||||
@@ -230,13 +198,10 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return html``;
|
||||
}
|
||||
const areas = this._getAreasWithDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -7,21 +6,15 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
|
||||
|
||||
import {
|
||||
computeDeviceName,
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
|
||||
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
@@ -45,7 +38,7 @@ const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`<mwc-list-item
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
export class HaDevicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -54,12 +47,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public devices?: DeviceRegistryEntry[];
|
||||
|
||||
@property() public areas?: AreaRegistryEntry[];
|
||||
|
||||
@property() public entities?: EntityRegistryEntry[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
@@ -84,6 +71,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;
|
||||
@@ -98,14 +93,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
deviceReg: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
entityReg: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"]
|
||||
deviceFilter: this["deviceFilter"],
|
||||
excludeDevices: this["excludeDevices"]
|
||||
): Device[] => {
|
||||
const devices = Object.values(deviceReg);
|
||||
const entities = Object.values(entityReg);
|
||||
|
||||
if (!devices.length) {
|
||||
return [
|
||||
{
|
||||
@@ -129,12 +128,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
let inputDevices = devices.filter(
|
||||
(device) => device.id === this.value || !device.disabled_by
|
||||
);
|
||||
@@ -164,6 +157,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];
|
||||
@@ -199,8 +198,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
deviceEntityLookup[device.id]
|
||||
),
|
||||
area:
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
device.area_id && device.area_id in areas
|
||||
? areas[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
}));
|
||||
if (!outputDevices.length) {
|
||||
@@ -216,7 +215,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)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -231,34 +230,21 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this.devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this.areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this.entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.devices && this.areas && this.entities) ||
|
||||
!this._init ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this.comboBox as any).items = this._getDevices(
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter
|
||||
this.deviceFilter,
|
||||
this.excludeDevices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
key
|
||||
)
|
||||
: formatAttributeValue(this.hass, key),
|
||||
|
||||
@@ -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,45 @@ 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 { 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;
|
||||
@@ -158,7 +182,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
entityState,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -185,19 +210,18 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
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({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user