mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-03 15:02:01 +00:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09982a9238 | |||
| ab1a58b3f3 | |||
| a7ff89385e | |||
| f3d41be3bf | |||
| b73707751a | |||
| 61bff43cdb | |||
| 0a0d08fa19 | |||
| ae29ba63ff | |||
| 0579cd8eb6 | |||
| 8c3eafec6d | |||
| b5c2e12016 | |||
| f7a13392cd | |||
| a2cdd592f1 | |||
| f04341a2a2 | |||
| 91bdc80a67 | |||
| b4824cc0a7 | |||
| 28f375c0d4 | |||
| da7ccac811 | |||
| a8ad921efd | |||
| 3b8f219800 | |||
| e36a2e1c70 | |||
| e06ea1047c | |||
| 99cb997d08 | |||
| ac3edd20f8 | |||
| 0d88d139f0 | |||
| b8d08ccb05 | |||
| 7c20316ba5 | |||
| fa633efc87 | |||
| 85d461f0fd | |||
| b55e1c9988 | |||
| 1da349a36d | |||
| 74f7139a09 | |||
| 2911cc77fa | |||
| ab20383a3a | |||
| 514cb9da9d | |||
| 7c52ac8ca7 | |||
| 07b4a44228 | |||
| 2b28a6c3f2 | |||
| 84f2e304cf | |||
| 18cd40ab01 | |||
| 8e3b1dc6ac | |||
| 5cc223a582 | |||
| 9a62a9217c | |||
| 70be747e9d | |||
| bb57a91494 | |||
| 7e22e6c0e2 | |||
| c93f910e56 | |||
| 8bf4ff5d25 | |||
| debc3adf19 | |||
| ae21017de8 | |||
| f15f518cc2 | |||
| 0e44417051 | |||
| 3581b43336 | |||
| 32b9676f97 | |||
| 7876642f35 | |||
| 0e3bcfad5e | |||
| cd1c273d5a | |||
| d92ac4b4b7 | |||
| bfecb1d4a9 | |||
| 69a8db00fa | |||
| bbda7affdc | |||
| 10c90d222d | |||
| 072f70b49f | |||
| 7f2a5ecc27 | |||
| a42f6f864a | |||
| a07772c514 | |||
| a6ab6e218f | |||
| ed96657085 | |||
| 50ca39722e | |||
| 7026e5b375 | |||
| 37e8e1b728 | |||
| 48369854af | |||
| 7715e01126 | |||
| e4ee108e14 | |||
| 407609c118 | |||
| 68cbaf6481 | |||
| b9b249a317 | |||
| df6578dfdf | |||
| b4a07f504c | |||
| 24b8078a9c | |||
| 2bd8a657b8 | |||
| 61724a52ba | |||
| 67d73261a4 | |||
| 419ba64bed | |||
| e87155bba3 | |||
| d8a3939e22 | |||
| 5141b6aea9 | |||
| 9aaf1bdd07 | |||
| a0e7c8e9bb | |||
| 8f482776b9 | |||
| 2c80183ded | |||
| 9bd5e25ee3 | |||
| 994193465f | |||
| 530b7ed457 | |||
| 24332e8288 | |||
| 980aaa2bac | |||
| 344856cbc4 | |||
| 06877cbaaa | |||
| e1ac7e98b0 | |||
| 4b2426ab77 | |||
| f9471d6b4c | |||
| 0645484258 | |||
| 2da3efb812 | |||
| e3674e550f | |||
| cff038fea4 | |||
| bed39fd8e9 | |||
| b8346d0286 | |||
| 5f80b74322 | |||
| 73e0f1e18d | |||
| 0b16b735e8 | |||
| c88dcf10b9 | |||
| 08bc23e2e7 | |||
| 97d49f06b1 | |||
| 50b727393d | |||
| 5c6dd2a697 |
@@ -5,6 +5,9 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
cooldown:
|
||||
default-days-before-reopen: 30
|
||||
default-days: 7
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- Dependencies
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -24,6 +27,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -59,6 +63,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -18,6 +18,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint and check format
|
||||
@@ -25,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -59,6 +64,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -77,6 +84,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -28,6 +32,7 @@ jobs:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
@@ -36,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
- dev
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -25,6 +28,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -60,6 +64,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -17,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -10,6 +10,9 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -22,6 +25,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: "Pull Request Labeler"
|
||||
|
||||
on: pull_request_target
|
||||
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
name: RelativeCI
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload stats
|
||||
upload-frontend-modern:
|
||||
name: Upload stats (frontend/modern)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
|
||||
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-modern.json
|
||||
|
||||
upload-frontend-legacy:
|
||||
name: Upload stats (frontend/legacy)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-legacy.json
|
||||
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -34,13 +36,12 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
uses: home-assistant/actions/helpers/verify-version@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
@@ -62,11 +63,10 @@ jobs:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
|
||||
|
||||
wheels-init:
|
||||
name: Init wheels build
|
||||
@@ -74,15 +74,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate requirements.txt
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
# Sleep to give pypi time to populate the new version across mirrors
|
||||
sleep 240
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
@@ -99,11 +101,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
@@ -113,8 +116,11 @@ jobs:
|
||||
- name: Build landing-page
|
||||
run: landing-page/script/build_landing_page
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
paths:
|
||||
- src/translations/en.json
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload
|
||||
@@ -15,6 +18,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -99,6 +99,44 @@ const lokaliseProjects = {
|
||||
frontend: "3420425759f6d6d241f598.13594006",
|
||||
};
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async function pollProcess(lokaliseApi, projectId, processId) {
|
||||
while (true) {
|
||||
const process = await lokaliseApi
|
||||
.queuedProcesses()
|
||||
.get(processId, { project_id: projectId });
|
||||
|
||||
const project =
|
||||
projectId === lokaliseProjects.backend ? "backend" : "frontend";
|
||||
|
||||
if (process.status === "finished") {
|
||||
console.log(`Lokalise export process for ${project} finished`);
|
||||
return process;
|
||||
}
|
||||
|
||||
if (process.status === "failed" || process.status === "cancelled") {
|
||||
throw new Error(
|
||||
`Lokalise export process for ${project} ${process.status}: ${process.message}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Lokalise export process for ${project} in progress...`,
|
||||
process.status,
|
||||
process.details?.items_to_process
|
||||
? `${Math.round(((process.details.items_processed || 0) / process.details.items_to_process) * 100)}% (${process.details.items_processed}/${process.details.items_to_process})`
|
||||
: ""
|
||||
);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, POLL_INTERVAL_MS);
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
let apiKey;
|
||||
try {
|
||||
@@ -118,55 +156,60 @@ gulp.task("fetch-lokalise", async function () {
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(lokaliseProjects).map(([project, projectId]) =>
|
||||
lokaliseApi
|
||||
.files()
|
||||
.download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
})
|
||||
.then((download) => fetch(download.bundle_url))
|
||||
.then((response) => {
|
||||
if (response.status === 200 || response.status === 0) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
Object.entries(lokaliseProjects).map(async ([project, projectId]) => {
|
||||
try {
|
||||
const exportProcess = await lokaliseApi
|
||||
.files()
|
||||
.async_download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
});
|
||||
|
||||
const finishedProcess = await pollProcess(
|
||||
lokaliseApi,
|
||||
projectId,
|
||||
exportProcess.process_id
|
||||
);
|
||||
|
||||
const bundleUrl = finishedProcess.details.download_url;
|
||||
|
||||
console.log(`Downloading translations from: ${bundleUrl}`);
|
||||
|
||||
const response = await fetch(bundleUrl);
|
||||
|
||||
if (response.status !== 200 && response.status !== 0) {
|
||||
throw new Error(response.statusText);
|
||||
})
|
||||
.then(JSZip.loadAsync)
|
||||
.then(async (contents) => {
|
||||
await mkdirPromise;
|
||||
return Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return Promise.resolve();
|
||||
}
|
||||
return file
|
||||
.async("nodebuffer")
|
||||
.then((content) =>
|
||||
fs.writeFile(
|
||||
path.join(
|
||||
inDir,
|
||||
project,
|
||||
filename.split("/").splice(-1)[0]
|
||||
),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Extracting translations...`);
|
||||
|
||||
const contents = await JSZip.loadAsync(await response.arrayBuffer());
|
||||
|
||||
await mkdirPromise;
|
||||
await Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return;
|
||||
}
|
||||
const content = await file.async("nodebuffer");
|
||||
await fs.writeFile(
|
||||
path.join(inDir, project, filename.split("/").splice(-1)[0]),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
);
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/input/ha-input";
|
||||
import "../../../../src/components/ha-button";
|
||||
|
||||
const seeFAQ = (qid) => html`
|
||||
@@ -123,11 +123,11 @@ export class HcConnect extends LitElement {
|
||||
To get started, enter your Home Assistant URL and click authorize.
|
||||
If you want a preview instead, click the show demo button.
|
||||
</p>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
label="Home Assistant URL"
|
||||
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -204,7 +204,7 @@ export class HcConnect extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleConnect() {
|
||||
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
|
||||
const inputEl = this.shadowRoot!.querySelector("ha-input")!;
|
||||
const value = inputEl.value || "";
|
||||
this.error = undefined;
|
||||
|
||||
@@ -319,7 +319,7 @@ export class HcConnect extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
import { mdiTelevision } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
import { mockConfigEntries } from "../../../../demo/src/stubs/config_entries";
|
||||
@@ -692,7 +692,11 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
([key, value]) => html`
|
||||
<ha-settings-row narrow slot=${slot}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
<span slot="description">${value?.description}</span>
|
||||
${value?.description
|
||||
? html`<span slot="description"
|
||||
>${value?.description}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
|
||||
@@ -19,7 +19,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Start designing with our <a href="https://www.figma.com/design/2WGI8IDGyxINjSV6NRvPur/Home-Assistant-Design-Kit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
## Developers
|
||||
|
||||
@@ -134,6 +134,21 @@ const CONFIGS = [
|
||||
entity: sensor.not_working
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lower minimum",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.brightness_high
|
||||
needle: true
|
||||
severity:
|
||||
green: 0
|
||||
yellow: 0.45
|
||||
red: 0.9
|
||||
min: -0.05
|
||||
name: " "
|
||||
max: 1.9
|
||||
unit: GBP/h`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-gauge-card")
|
||||
|
||||
@@ -422,7 +422,6 @@ export class DemoEntityState extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.columns=${this._columns(this.hass)}
|
||||
.data=${this._rows()}
|
||||
auto-height
|
||||
|
||||
+9
-11
@@ -30,7 +30,7 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.1",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@swc/helpers": "0.5.20",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -129,7 +129,6 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
@@ -150,8 +149,8 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.5",
|
||||
"@rspack/core": "1.7.9",
|
||||
"@rsdoctor/rspack-plugin": "1.5.6",
|
||||
"@rspack/core": "1.7.10",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -169,9 +168,8 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.0",
|
||||
"@vitest/coverage-v8": "4.1.2",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -206,13 +204,13 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "21.0.3",
|
||||
"tar": "7.5.12",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"typescript": "6.0.2",
|
||||
"typescript-eslint": "8.57.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.0",
|
||||
"vitest": "4.1.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
/// <reference types="chromecast-caf-sender" />
|
||||
|
||||
import type { Auth } from "home-assistant-js-websocket";
|
||||
import { castApiAvailable } from "./cast_framework";
|
||||
|
||||
@@ -32,6 +32,12 @@ const YAML_ONLY_THEMES_COLORS = new Set([
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Compose a CSS variable out of a theme color
|
||||
* @param color - Theme color (examples: `red`, `primary-text`)
|
||||
* @returns CSS variable in `--xxx-color` format;
|
||||
* initial color if not found in theme colors
|
||||
*/
|
||||
export function computeCssVariableName(color: string): string {
|
||||
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
|
||||
return `--${color}-color`;
|
||||
@@ -39,6 +45,12 @@ export function computeCssVariableName(color: string): string {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a CSS variable out of a theme color & then resolve it
|
||||
* @param color - Theme color (examples: `red`, `primary-text`)
|
||||
* @returns Resolved CSS variable in `var(--xxx-color)` format;
|
||||
* initial color if not found in theme colors
|
||||
*/
|
||||
export function computeCssColor(color: string): string {
|
||||
const cssVarName = computeCssVariableName(color);
|
||||
if (cssVarName !== color) {
|
||||
@@ -47,6 +59,22 @@ export function computeCssColor(color: string): string {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a color from document's styles
|
||||
* @param color - Named theme color (examples: `red`, `primary-text`)
|
||||
* @returns Resolved color; initial color if not found in document's styles
|
||||
*/
|
||||
export function resolveThemeColor(color: string): string {
|
||||
const cssColor = computeCssVariableName(color);
|
||||
if (cssColor.startsWith("--")) {
|
||||
const resolved = getComputedStyle(document.body)
|
||||
.getPropertyValue(cssColor)
|
||||
.trim();
|
||||
return resolved || color;
|
||||
}
|
||||
return cssColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid color.
|
||||
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import colors from "color-name";
|
||||
import { expandHex } from "./hex";
|
||||
import { resolveThemeColor } from "./compute-color";
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
@@ -130,26 +131,43 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
|
||||
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||
hsv2rgb([hs[0], hs[1], 255]);
|
||||
|
||||
export function theme2hex(themeColor: string): string {
|
||||
if (themeColor.startsWith("#")) {
|
||||
if (themeColor.length === 4 || themeColor.length === 5) {
|
||||
const c = themeColor;
|
||||
/**
|
||||
* Attempt to get a HEX color from a color defined in different formats:
|
||||
* HEX, rgb/rgba, named color
|
||||
* @param color - Color (HEX, rgb/rgba, named color) to be converted to HEX
|
||||
* @returns HEX color
|
||||
*/
|
||||
export function theme2hex(color: string): string {
|
||||
// Attempting to find a HEX pattern in the input string
|
||||
if (color.startsWith("#")) {
|
||||
if (color.length === 4 || color.length === 5) {
|
||||
const c = color;
|
||||
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
|
||||
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||||
}
|
||||
if (themeColor.length === 9) {
|
||||
if (color.length === 9) {
|
||||
// Ignore alpha channel.
|
||||
return themeColor.substring(0, 7);
|
||||
return color.substring(0, 7);
|
||||
}
|
||||
return themeColor;
|
||||
return color;
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor.toLowerCase()];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
// Attempting to find a match in a HA Frontend theme colors
|
||||
const themeColor = resolveThemeColor(color.toLowerCase());
|
||||
if (themeColor !== color.toLowerCase()) {
|
||||
// theme color is recognized, now re-attempt
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
// Attempting to find a match in a web colors array
|
||||
const rgbFromWebColor = colors[color.toLowerCase()];
|
||||
if (rgbFromWebColor) {
|
||||
// HEX color is recognized for the input named color
|
||||
return rgb2hex(rgbFromWebColor);
|
||||
}
|
||||
|
||||
// Attempting to find an RGB pattern in the input string
|
||||
const rgbMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
@@ -158,5 +176,5 @@ export function theme2hex(themeColor: string): string {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { wcagLuminance, wcagContrast } from "culori";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
/**
|
||||
* Calculates the luminosity of an RGB color.
|
||||
@@ -48,3 +49,13 @@ export const getRGBContrastRatio = (
|
||||
rgb1: [number, number, number],
|
||||
rgb2: [number, number, number]
|
||||
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
|
||||
|
||||
/**
|
||||
* Returns a contrasted color (black or white) based on the luminance of another color
|
||||
* @param color - Color (HEX, rgb/rgba, named color) to calculate a contrasted color
|
||||
* @returns HEX color ("#000000" for dark backgrounds, "#ffffff" for light backgrounds)
|
||||
*/
|
||||
export const getContrastedColorHex = (color: string): string => {
|
||||
const lum = wcagLuminance(theme2hex(color));
|
||||
return lum > 0.5 ? "#000000" : "#ffffff";
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { listenMediaQuery } from "../dom/media_query";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
||||
import type {
|
||||
Condition,
|
||||
ConditionContext,
|
||||
} from "../../panels/lovelace/common/validate-condition";
|
||||
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
||||
import { extractMediaQueries, extractTimeConditions } from "./extract";
|
||||
import { calculateNextTimeUpdate } from "./time-calculator";
|
||||
@@ -19,7 +22,8 @@ export function setupMediaQueryListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
const mediaQueries = extractMediaQueries(conditions);
|
||||
|
||||
@@ -36,7 +40,8 @@ export function setupMediaQueryListeners(
|
||||
if (hasOnlyMediaQuery) {
|
||||
onUpdate(matches);
|
||||
} else {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
const context = getContext?.() ?? {};
|
||||
const conditionsMet = checkConditionsMet(conditions, hass, context);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
});
|
||||
@@ -51,7 +56,8 @@ export function setupTimeListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
const timeConditions = extractTimeConditions(conditions);
|
||||
|
||||
@@ -70,7 +76,8 @@ export function setupTimeListeners(
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (delay <= MAX_TIMEOUT_DELAY) {
|
||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||
const context = getContext?.() ?? {};
|
||||
const conditionsMet = checkConditionsMet(conditions, hass, context);
|
||||
onUpdate(conditionsMet);
|
||||
}
|
||||
scheduleUpdate();
|
||||
@@ -87,3 +94,17 @@ export function setupTimeListeners(
|
||||
scheduleUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up all condition listeners (media query, time) for conditional visibility.
|
||||
*/
|
||||
export function setupConditionListeners(
|
||||
conditions: Condition[],
|
||||
hass: HomeAssistant,
|
||||
addListener: (unsub: () => void) => void,
|
||||
onUpdate: (conditionsMet: boolean) => void,
|
||||
getContext?: () => ConditionContext
|
||||
): void {
|
||||
setupMediaQueryListeners(conditions, hass, addListener, onUpdate, getContext);
|
||||
setupTimeListeners(conditions, hass, addListener, onUpdate, getContext);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const isLoadedIntegration = (
|
||||
) =>
|
||||
!page.component ||
|
||||
ensureArray(page.component).some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isNotLoadedIntegration = (
|
||||
@@ -23,7 +23,7 @@ export const isNotLoadedIntegration = (
|
||||
) =>
|
||||
!page.not_component ||
|
||||
!ensureArray(page.not_component).some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
isComponentLoaded(hass.config, integration)
|
||||
);
|
||||
|
||||
export const isCore = (page: PageNavigation) => page.core;
|
||||
|
||||
@@ -2,6 +2,6 @@ import type { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a component is loaded. */
|
||||
export const isComponentLoaded = (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
component: string
|
||||
): boolean => hass && hass.config.components.includes(component);
|
||||
): boolean => hassConfig && hassConfig.components.includes(component);
|
||||
|
||||
@@ -14,24 +14,25 @@ export const computeDeviceName = (
|
||||
|
||||
export const computeDeviceNameDisplay = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant,
|
||||
localize: HomeAssistant["localize"],
|
||||
hassStates: HomeAssistant["states"],
|
||||
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) =>
|
||||
computeDeviceName(device) ||
|
||||
(entities && fallbackDeviceName(hass, entities)) ||
|
||||
hass.localize("ui.panel.config.devices.unnamed_device", {
|
||||
type: hass.localize(
|
||||
(entities && fallbackDeviceName(hassStates, entities)) ||
|
||||
localize("ui.panel.config.devices.unnamed_device", {
|
||||
type: localize(
|
||||
`ui.panel.config.devices.type.${device.entry_type || "device"}`
|
||||
),
|
||||
});
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
hass: HomeAssistant,
|
||||
hassStates: HomeAssistant["states"],
|
||||
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
|
||||
) => {
|
||||
for (const entity of entities || []) {
|
||||
const entityId = typeof entity === "string" ? entity : entity.entity_id;
|
||||
const stateObj = hass.states[entityId];
|
||||
const stateObj = hassStates[entityId];
|
||||
if (stateObj) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
@@ -18,22 +21,49 @@ export const computeEntityName = (
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
if (entry.name != null) {
|
||||
return entry.name;
|
||||
const name =
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (fallbackStateObj) {
|
||||
return computeStateName(fallbackStateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if ("original_name" in entry && entry.original_name != null) {
|
||||
return String(entry.original_name);
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// If the device name is the same as the entity name, consider empty entity name
|
||||
if (deviceName === name) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
// Remove the device name from the entity name if it starts with it
|
||||
if (deviceName && name) {
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
): boolean => !computeEntityName(stateObj, entities);
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): boolean => !computeEntityName(stateObj, entities, devices);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
@@ -29,14 +30,23 @@ export interface EntityNameOptions {
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem | EntityNameItem[] | undefined,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
options?: EntityNameOptions
|
||||
) => {
|
||||
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
|
||||
if (typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// If no name config is provided, fall back to the friendly name
|
||||
if (!name) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
let items = ensureArray(name);
|
||||
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
@@ -45,7 +55,7 @@ export const computeEntityNameDisplay = (
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities);
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
|
||||
// If entity uses device name, and device is not already included, replace it with device name
|
||||
if (useDeviceName) {
|
||||
@@ -91,7 +101,7 @@ export const computeEntityNameList = (
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities);
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
|
||||
@@ -142,9 +142,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
minusSign: "value",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
@@ -153,7 +154,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
interface DeviceContext {
|
||||
device: DeviceRegistryEntry;
|
||||
area: AreaRegistryEntry | null;
|
||||
floor: FloorRegistryEntry | null;
|
||||
}
|
||||
|
||||
export const getDeviceContext = (
|
||||
export const getDeviceArea = (
|
||||
device: DeviceRegistryEntry,
|
||||
hass: HomeAssistant
|
||||
): DeviceContext => {
|
||||
areas: HomeAssistant["areas"]
|
||||
): AreaRegistryEntry | undefined => {
|
||||
const areaId = device.area_id;
|
||||
const area = areaId ? hass.areas[areaId] : undefined;
|
||||
const floorId = area?.floor_id;
|
||||
const floor = floorId ? hass.floors[floorId] : undefined;
|
||||
|
||||
return {
|
||||
device: device,
|
||||
area: area || null,
|
||||
floor: floor || null,
|
||||
};
|
||||
return areaId ? areas[areaId] : undefined;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const isDeletableEntity = (
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
return !!(
|
||||
isComponentLoaded(hass, domain) &&
|
||||
isComponentLoaded(hass.config, domain) &&
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
);
|
||||
@@ -56,7 +56,7 @@ export const deleteEntity = (
|
||||
const domain = computeDomain(entity_id);
|
||||
const entityRegEntry = entityRegistry.find((e) => e.entity_id === entity_id);
|
||||
if (isHelperDomain(domain)) {
|
||||
if (isComponentLoaded(hass, domain)) {
|
||||
if (isComponentLoaded(hass.config, domain)) {
|
||||
if (
|
||||
entityRegEntry &&
|
||||
fetchedHelpers.some((e) => e.id === entityRegEntry.unique_id)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Indicates whether the current browser has native ElementInternals support.
|
||||
*/
|
||||
export const nativeElementInternalsSupported =
|
||||
Boolean(globalThis.ElementInternals) &&
|
||||
globalThis.HTMLElement?.prototype.attachInternals
|
||||
?.toString()
|
||||
.includes("[native code]");
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Indicates whether the current browser supports the Popover API.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
||||
*/
|
||||
export const popoverSupported = globalThis?.HTMLElement?.prototype
|
||||
? Object.prototype.hasOwnProperty.call(
|
||||
globalThis.HTMLElement.prototype,
|
||||
"popover"
|
||||
)
|
||||
: false;
|
||||
@@ -41,7 +41,7 @@ export const protocolIntegrationPicked = async (
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zwave_js") ||
|
||||
!isComponentLoaded(hass.config, "zwave_js") ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
@@ -90,7 +90,7 @@ export const protocolIntegrationPicked = async (
|
||||
).filter((e) => !e.disabled_by);
|
||||
|
||||
if (
|
||||
!isComponentLoaded(hass, "zha") ||
|
||||
!isComponentLoaded(hass.config, "zha") ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
@@ -139,7 +139,7 @@ export const protocolIntegrationPicked = async (
|
||||
})
|
||||
).filter((e) => !e.disabled_by);
|
||||
if (
|
||||
!isComponentLoaded(hass, domain) ||
|
||||
!isComponentLoaded(hass.config, domain) ||
|
||||
(!options?.config_entry && !entries?.length)
|
||||
) {
|
||||
// If the component isn't loaded, ask them to load the integration first
|
||||
|
||||
@@ -71,13 +71,6 @@ export const formatNumberToParts = (
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num))
|
||||
|
||||
@@ -5,12 +5,41 @@ import {
|
||||
formatDateMonthYear,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
export function getPeriodicAxisLabelConfig(
|
||||
period: string,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
):
|
||||
| {
|
||||
formatter: (value: number) => string;
|
||||
}
|
||||
| undefined {
|
||||
if (period === "month") {
|
||||
return {
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (period === "year") {
|
||||
return {
|
||||
formatter: (value: number) =>
|
||||
formatDateYear(new Date(value), locale, config),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
locale: FrontendLocaleData,
|
||||
|
||||
@@ -91,6 +91,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
private _longPressTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _longPressTriggered = false;
|
||||
|
||||
private _shouldResizeChart = false;
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
@@ -128,6 +132,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._legendPointerCancel();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
@@ -302,22 +307,31 @@ export class HaChartBase extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
private _getLegendItems() {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend).find(
|
||||
(l) => l.show && l.type === "custom"
|
||||
) as CustomLegendOption | undefined;
|
||||
if (!legend) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items =
|
||||
return (
|
||||
legend.data ||
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => ({ id: d.id, name: d.name }));
|
||||
.map((d) => ({ id: d.id, name: d.name }))
|
||||
);
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data!);
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -362,6 +376,11 @@ export class HaChartBase extends LitElement {
|
||||
return html`<li
|
||||
.id=${id}
|
||||
@click=${this._legendClick}
|
||||
@pointerdown=${this._legendPointerDown}
|
||||
@pointerup=${this._legendPointerCancel}
|
||||
@pointerleave=${this._legendPointerCancel}
|
||||
@pointercancel=${this._legendPointerCancel}
|
||||
@contextmenu=${this._legendContextMenu}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
.title=${name}
|
||||
>
|
||||
@@ -587,10 +606,7 @@ export class HaChartBase extends LitElement {
|
||||
id: "dataZoom",
|
||||
type: "inside",
|
||||
orient: "horizontal",
|
||||
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
|
||||
// It rescales the Y-axis to the visible data while keeping one point
|
||||
// just outside each boundary to avoid line gaps at the zoom edges.
|
||||
filterMode: "boundaryFilter" as any,
|
||||
filterMode: this._getDataZoomFilterMode() as any,
|
||||
xAxisIndex: 0,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
@@ -598,6 +614,23 @@ export class HaChartBase extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
// "boundaryFilter" is a custom mode added via axis-proxy-patch.ts.
|
||||
// It rescales the Y-axis to the visible data while keeping one point
|
||||
// just outside each boundary to avoid line gaps at the zoom edges.
|
||||
// Use "filter" for bar charts since boundaryFilter causes rendering issues.
|
||||
// Use "weakFilter" for other types (e.g. custom/timeline) so bars
|
||||
// spanning the visible range boundary are kept.
|
||||
private _getDataZoomFilterMode(): string {
|
||||
const series = ensureArray(this.data);
|
||||
if (series.every((s) => s.type === "line")) {
|
||||
return "boundaryFilter";
|
||||
}
|
||||
if (series.some((s) => s.type === "bar")) {
|
||||
return "filter";
|
||||
}
|
||||
return "weakFilter";
|
||||
}
|
||||
|
||||
private _createOptions(): ECOption {
|
||||
let xAxis = this.options?.xAxis;
|
||||
if (xAxis) {
|
||||
@@ -632,7 +665,7 @@ export class HaChartBase extends LitElement {
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
minInterval: axis.minInterval ?? minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
@@ -1022,11 +1055,52 @@ export class HaChartBase extends LitElement {
|
||||
fireEvent(this, "chart-zoom", { start, end });
|
||||
}
|
||||
|
||||
private _legendClick(ev: any) {
|
||||
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
|
||||
private _legendPointerDown(ev: PointerEvent) {
|
||||
// Mouse uses Ctrl/Cmd+click instead
|
||||
if (ev.pointerType === "mouse") {
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this._longPressTriggered = false;
|
||||
this._longPressTimer = setTimeout(() => {
|
||||
this._longPressTriggered = true;
|
||||
this._longPressTimer = undefined;
|
||||
this._soloLegend(id);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _legendPointerCancel() {
|
||||
if (this._longPressTimer) {
|
||||
clearTimeout(this._longPressTimer);
|
||||
this._longPressTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _legendContextMenu(ev: Event) {
|
||||
if (this._longPressTimer || this._longPressTriggered) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private _legendClick(ev: MouseEvent) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._longPressTriggered) {
|
||||
this._longPressTriggered = false;
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
|
||||
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
if (soloModifier) {
|
||||
this._soloLegend(id);
|
||||
return;
|
||||
}
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
@@ -1041,6 +1115,60 @@ export class HaChartBase extends LitElement {
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _soloLegend(id: string) {
|
||||
const allIds = this._getAllLegendIds();
|
||||
const clickedIds = this._getAllIdsFromLegend(this.options, id);
|
||||
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
|
||||
|
||||
const clickedIsOnlyVisible =
|
||||
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
|
||||
otherIds.every((i) => this._hiddenDatasets.has(i));
|
||||
|
||||
if (clickedIsOnlyVisible) {
|
||||
// Already solo'd on this item — restore all series to visible
|
||||
for (const hiddenId of [...this._hiddenDatasets]) {
|
||||
this._hiddenDatasets.delete(hiddenId);
|
||||
fireEvent(this, "dataset-unhidden", { id: hiddenId });
|
||||
}
|
||||
} else {
|
||||
// Solo: hide every other series, unhide clicked if it was hidden
|
||||
for (const otherId of otherIds) {
|
||||
if (!this._hiddenDatasets.has(otherId)) {
|
||||
this._hiddenDatasets.add(otherId);
|
||||
fireEvent(this, "dataset-hidden", { id: otherId });
|
||||
}
|
||||
}
|
||||
for (const clickedId of clickedIds) {
|
||||
if (this._hiddenDatasets.has(clickedId)) {
|
||||
this._hiddenDatasets.delete(clickedId);
|
||||
fireEvent(this, "dataset-unhidden", { id: clickedId });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _getAllLegendIds(): string[] {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
const allIds = new Set<string>();
|
||||
for (const item of items) {
|
||||
const primaryId =
|
||||
typeof item === "string"
|
||||
? item
|
||||
: ((item.id as string) ?? (item.name as string) ?? "");
|
||||
for (const expandedId of this._getAllIdsFromLegend(
|
||||
this.options,
|
||||
primaryId
|
||||
)) {
|
||||
allIds.add(expandedId);
|
||||
}
|
||||
}
|
||||
return [...allIds];
|
||||
}
|
||||
|
||||
private _toggleExpandedLegend() {
|
||||
this.expandLegend = !this.expandLegend;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface NetworkData {
|
||||
categories?: { name: string; symbol: string }[];
|
||||
}
|
||||
|
||||
const PHYSICS_DISABLE_THRESHOLD = 512;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@@ -94,7 +96,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled = true;
|
||||
@state() private _physicsEnabled?: boolean;
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
@@ -122,6 +124,14 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (this._physicsEnabled === undefined && this.data?.nodes?.length > 1) {
|
||||
this._physicsEnabled =
|
||||
this.data.nodes.length <= PHYSICS_DISABLE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!GraphChart || !this.data.nodes?.length) {
|
||||
return nothing;
|
||||
@@ -138,7 +148,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled,
|
||||
this._physicsEnabled ?? false,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile,
|
||||
|
||||
@@ -28,6 +28,13 @@ const safeParseFloat = (value) => {
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const CLIMATE_MODE_CONFIGS = [
|
||||
{ mode: "heat", action: "heating", cssVar: "--state-climate-heat-color" },
|
||||
{ mode: "cool", action: "cooling", cssVar: "--state-climate-cool-color" },
|
||||
{ mode: "dry", action: "drying", cssVar: "--state-climate-dry-color" },
|
||||
{ mode: "fan_only", action: "fan", cssVar: "--state-climate-fan_only-color" },
|
||||
] as const;
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -429,23 +436,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(entityState) => entityState.attributes?.hvac_action
|
||||
);
|
||||
|
||||
const isHeating =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "heat"
|
||||
: (entityState: LineChartState) => entityState.state === "heat";
|
||||
const isCooling =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "cool"
|
||||
: (entityState: LineChartState) => entityState.state === "cool";
|
||||
|
||||
const hasHeat = states.states.some(isHeating);
|
||||
const hasCool = states.states.some(isCooling);
|
||||
const activeModes = CLIMATE_MODE_CONFIGS.map(
|
||||
({ mode, action, cssVar }) => {
|
||||
const isActive =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === mode
|
||||
: (entityState: LineChartState) => entityState.state === mode;
|
||||
return { action, cssVar, isActive };
|
||||
}
|
||||
).filter(({ isActive }) => states.states.some(isActive));
|
||||
// We differentiate between thermostats that have a target temperature
|
||||
// range versus ones that have just a target temperature
|
||||
|
||||
@@ -466,33 +468,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
if (hasHeat) {
|
||||
for (const { action, cssVar } of activeModes) {
|
||||
addDataSet(
|
||||
states.entity_id + "-heating",
|
||||
`${states.entity_id}-${action}`,
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
? this.hass.localize(`ui.card.climate.${action}`, {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
`component.climate.entity_component._.state_attributes.hvac_action.state.${action}`
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
computedStyles.getPropertyValue(cssVar),
|
||||
true
|
||||
);
|
||||
// The "heating" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
// The "cooling" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
|
||||
if (hasTargetRange) {
|
||||
@@ -540,11 +528,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.current_temperature
|
||||
);
|
||||
const series = [curTemp];
|
||||
if (hasHeat) {
|
||||
series.push(isHeating(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasCool) {
|
||||
series.push(isCooling(entityState) ? curTemp : null);
|
||||
for (const { isActive } of activeModes) {
|
||||
series.push(isActive(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasTargetRange) {
|
||||
const targetHigh = safeParseFloat(
|
||||
|
||||
@@ -105,7 +105,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
if (!isComponentLoaded(this.hass.config, "history")) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
|
||||
@@ -148,7 +149,7 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
if (!isComponentLoaded(this.hass.config, "history")) {
|
||||
return html`<div class="info">
|
||||
${this.hass.localize("ui.components.history_charts.history_disabled")}
|
||||
</div>`;
|
||||
@@ -293,6 +294,22 @@ export class StatisticsChart extends LitElement {
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
...(this.period === "month" && {
|
||||
minInterval: 28 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"month",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
...(this.period === "year" && {
|
||||
minInterval: 365 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"year",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { computeCssColor } from "../../common/color/compute-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
@@ -53,16 +52,15 @@ class HaDataTableLabels extends LitElement {
|
||||
}
|
||||
|
||||
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
|
||||
const color = label?.color ? computeCssColor(label.color) : undefined;
|
||||
return html`
|
||||
<ha-label
|
||||
dense
|
||||
role="button"
|
||||
tabindex="0"
|
||||
.color=${label.color}
|
||||
.item=${label}
|
||||
@click=${clickAction ? this._labelClicked : undefined}
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label?.icon
|
||||
@@ -102,10 +100,6 @@ class HaDataTableLabels extends LitElement {
|
||||
position: fixed;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
.plus {
|
||||
--ha-label-background-color: transparent;
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -21,9 +22,10 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { localeContext, localizeContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
@@ -104,9 +106,13 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private _localize?: ContextType<typeof localizeContext>;
|
||||
|
||||
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
|
||||
@state()
|
||||
@consume({ context: localeContext, subscribe: true })
|
||||
private _locale?: ContextType<typeof localeContext>;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -378,8 +384,6 @@ export class HaDataTable extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const localize = this.localizeFunc || this.hass.localize;
|
||||
|
||||
const columns = this._sortedColumns(this.columns, this.columnOrder);
|
||||
|
||||
const renderRow = (row: DataTableRowData, index: number) =>
|
||||
@@ -503,7 +507,8 @@ export class HaDataTable extends LitElement {
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
${this.noDataText ||
|
||||
localize("ui.components.data-table.no-data")}
|
||||
this._localize?.("ui.components.data-table.no-data") ||
|
||||
"No data"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -515,7 +520,8 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._groupData(
|
||||
this._filteredData,
|
||||
localize,
|
||||
this._localize,
|
||||
this._locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
@@ -685,7 +691,7 @@ export class HaDataTable extends LitElement {
|
||||
this._sortColumns[this.sortColumn],
|
||||
this.sortDirection,
|
||||
this.sortColumn,
|
||||
this.hass.locale.language
|
||||
this._locale?.language
|
||||
)
|
||||
: filteredData;
|
||||
|
||||
@@ -711,7 +717,8 @@ export class HaDataTable extends LitElement {
|
||||
private _groupData = memoizeOne(
|
||||
(
|
||||
data: DataTableRowData[],
|
||||
localize: LocalizeFunc,
|
||||
localize: LocalizeFunc | undefined,
|
||||
locale: FrontendLocaleData | undefined,
|
||||
appendRow,
|
||||
groupColumn: string | undefined,
|
||||
groupOrder: string[] | undefined,
|
||||
@@ -735,11 +742,7 @@ export class HaDataTable extends LitElement {
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (!groupOrder && isGroupSortColumn) {
|
||||
const comparison = stringCompare(
|
||||
a,
|
||||
b,
|
||||
this.hass.locale.language
|
||||
);
|
||||
const comparison = stringCompare(a, b, locale?.language);
|
||||
if (sortDirection === "asc") {
|
||||
return comparison;
|
||||
}
|
||||
@@ -760,7 +763,7 @@ export class HaDataTable extends LitElement {
|
||||
return stringCompare(
|
||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||
this.hass.locale.language
|
||||
locale?.language
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
@@ -787,14 +790,15 @@ export class HaDataTable extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
.label=${this.hass.localize(
|
||||
.label=${localize?.(
|
||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||
)}
|
||||
) || (collapsed ? "Expand" : "Collapse")}
|
||||
class=${collapsed ? "collapsed" : ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? localize("ui.components.data-table.ungrouped")
|
||||
? localize?.("ui.components.data-table.ungrouped") ||
|
||||
"Ungrouped"
|
||||
: groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
@@ -863,7 +867,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
const groupedData = this._groupData(
|
||||
this._filteredData,
|
||||
this.localizeFunc || this.hass.localize,
|
||||
this._localize,
|
||||
this._locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import {
|
||||
formatCallyDateRange,
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
localizeContext,
|
||||
} from "../../data/context";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-filter-chip";
|
||||
import type { HaFilterChip } from "../chips/ha-filter-chip";
|
||||
import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
@@ -27,11 +32,12 @@ import "../ha-icon-button-prev";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
import { datePickerStyles, dateRangePickerStyles } from "./styles";
|
||||
|
||||
@customElement("date-range-picker")
|
||||
export class DateRangePicker extends LitElement {
|
||||
export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: false }) public ranges?: DateRangePickerRanges | false;
|
||||
|
||||
@property({ attribute: false }) public startDate?: Date;
|
||||
@@ -69,6 +75,8 @@ export class DateRangePicker extends LitElement {
|
||||
to: { hours: 23, minutes: 59 },
|
||||
};
|
||||
|
||||
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -100,16 +108,38 @@ export class DateRangePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderRanges() {
|
||||
if (this._isMobileSize) {
|
||||
return html`
|
||||
<ha-chip-set class="ha-scrollbar">
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-filter-chip
|
||||
.index=${index}
|
||||
.range=${range}
|
||||
@click=${this._clickDateRangeChip}
|
||||
>
|
||||
${name}
|
||||
</ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div class="picker">
|
||||
${this.ranges !== false && this.ranges
|
||||
? html`<div class="date-range-ranges">
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list>
|
||||
</div>`
|
||||
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
|
||||
: nothing}
|
||||
<div class="range">
|
||||
<calendar-range
|
||||
@@ -153,6 +183,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="from"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
@@ -163,6 +194,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
@@ -200,6 +232,14 @@ export class DateRangePicker extends LitElement {
|
||||
let endDate = new Date(`${dates[1]}T23:59:00`);
|
||||
|
||||
if (this.timePicker) {
|
||||
const timeInputs = this._timeInputs;
|
||||
if (
|
||||
timeInputs &&
|
||||
![...timeInputs].every((input) => input.reportValidity())
|
||||
) {
|
||||
// If we have time inputs, and they don't all report valid, don't save
|
||||
return;
|
||||
}
|
||||
startDate.setHours(this._timeValue.from.hours);
|
||||
startDate.setMinutes(this._timeValue.from.minutes);
|
||||
endDate.setHours(this._timeValue.to.hours);
|
||||
@@ -257,31 +297,38 @@ export class DateRangePicker extends LitElement {
|
||||
this._focusDate = undefined;
|
||||
}
|
||||
|
||||
private _clickDateRangeChip(ev: Event) {
|
||||
const chip = ev.target as HaFilterChip & {
|
||||
index: number;
|
||||
range: [Date, Date];
|
||||
};
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(ev: CustomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._dateValue = formatCallyDateRange(
|
||||
dateRange[0],
|
||||
dateRange[1],
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
);
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
startDate: dateRange[0],
|
||||
endDate: dateRange[1],
|
||||
startDate: range[0],
|
||||
endDate: range[1],
|
||||
},
|
||||
});
|
||||
fireEvent(this, "preset-selected", {
|
||||
index: ev.detail.index,
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChangeTime(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const time = ev.detail.value;
|
||||
const type = (ev.target as HaBaseTimeInput).id;
|
||||
const target = ev.target as HaBaseTimeInput;
|
||||
const type = target.id;
|
||||
if (time) {
|
||||
if (!this._timeValue) {
|
||||
this._timeValue = {
|
||||
@@ -298,20 +345,48 @@ export class DateRangePicker extends LitElement {
|
||||
static styles = [
|
||||
datePickerStyles,
|
||||
dateRangePickerStyles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-right: var(--ha-border-width-sm) solid var(--divider-color);
|
||||
min-width: 140px;
|
||||
flex: 0 1 30%;
|
||||
}
|
||||
|
||||
.range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.picker {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
margin-top: var(--ha-space-5);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.range {
|
||||
flex-basis: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.times {
|
||||
@@ -326,12 +401,6 @@ export class DateRangePicker extends LitElement {
|
||||
padding: var(--ha-space-2);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.date-range-ranges {
|
||||
max-width: 30%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,33 +80,6 @@ export const datePickerStyles = css`
|
||||
text-align: center;
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
calendar-month {
|
||||
min-height: calc(34px * 7);
|
||||
}
|
||||
calendar-month::part(day) {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
calendar-month::part(button) {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
calendar-month::part(range-inner),
|
||||
calendar-month::part(range-start),
|
||||
calendar-month::part(range-end),
|
||||
calendar-month::part(selected),
|
||||
calendar-month::part(selected):hover {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.month-year {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const dateRangePickerStyles = css`
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
type DevicePickerItem,
|
||||
} from "../../data/device/device_picker";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
@@ -154,7 +154,7 @@ export class HaDevicePicker extends LitElement {
|
||||
return html`<span slot="headline">${deviceId}</span>`;
|
||||
}
|
||||
|
||||
const { area } = getDeviceContext(device, this.hass);
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
@@ -17,12 +15,14 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
import "../input/ha-input";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
@@ -73,10 +73,291 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
@state() private _mode?: "composed" | "custom";
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (this._mode === undefined) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const modeButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_composed"
|
||||
),
|
||||
value: "composed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_custom"
|
||||
),
|
||||
value: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()}
|
||||
</div>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTextInput() {
|
||||
const items = this._items;
|
||||
const value =
|
||||
items.length === 1 && items[0].type === "text" ? items[0].text || "" : "";
|
||||
return html`
|
||||
<ha-input
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${value}
|
||||
@input=${this._textInputChanged}
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="field">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._mode = ev.detail.value as "composed" | "custom";
|
||||
}
|
||||
|
||||
private _textInputChanged(ev: Event) {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const newValue: EntityNameItem[] = value
|
||||
? [{ type: "text", text: value }]
|
||||
: [];
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
private _validTypes = memoizeOne((entityId?: string) => {
|
||||
const options = new Set<string>(["text"]);
|
||||
if (!entityId) {
|
||||
@@ -161,157 +442,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [...DEFAULT_ENTITY_NAME];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
const item = this._items[this._editIndex];
|
||||
@@ -362,58 +492,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
@@ -421,13 +499,42 @@ export class HaEntityNamePicker extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
ha-generic-picker,
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.container:after {
|
||||
.field:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -445,30 +552,25 @@ export class HaEntityNamePicker extends LitElement {
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
:host([disabled]) .field:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
.field:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import "../ha-tooltip";
|
||||
import "./state-badge";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@@ -22,7 +21,7 @@ class StateInfo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -94,7 +94,7 @@ class HaAddonPicker extends LitElement {
|
||||
|
||||
private async _getApps() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
this._addons = addonsInfo.addons
|
||||
.filter((addon) => addon.version)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
@@ -38,6 +39,8 @@ class HaAlert extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
public render() {
|
||||
@@ -65,7 +68,7 @@ class HaAlert extends LitElement {
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismissClicked}
|
||||
label="Dismiss alert"
|
||||
.label=${this.localize!("ui.common.dismiss_alert")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
@@ -267,7 +267,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
: item.domain
|
||||
? html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
.deviceClass=${item.deviceClass}
|
||||
></ha-domain-icon>`
|
||||
|
||||
@@ -56,7 +56,10 @@ class HaAttributeValue extends LitElement {
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts.find((part) => part.type === "value")?.value;
|
||||
return parts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
|
||||
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
@property({ attribute: "placeholder-labels", type: Boolean })
|
||||
public placeholderLabels = false;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: HaInput[];
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -145,7 +145,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
const inputs = this._inputs;
|
||||
if (!inputs) return true;
|
||||
return [...inputs].every((input) => input.reportValidity());
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -399,7 +401,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
.time-separator,
|
||||
ha-icon-button {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
background-color: var(--ha-color-form-background);
|
||||
color: var(--ha-color-text-secondary);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaButton extends Button {
|
||||
.button {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
transition: background-color var(--ha-animation-duration-fast)
|
||||
ease-out;
|
||||
|
||||
@@ -79,7 +79,6 @@ class HaConfigEntryPicker extends LitElement {
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
@@ -115,7 +114,6 @@ class HaConfigEntryPicker extends LitElement {
|
||||
slot="headline"
|
||||
>${item?.icon
|
||||
? html`<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${item.icon!}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`
|
||||
|
||||
@@ -123,6 +123,9 @@ export class HaDateInput extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
min-width: 0px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import {
|
||||
authContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
themesContext,
|
||||
} from "../data/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
domainIcon,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import "./ha-icon";
|
||||
|
||||
@customElement("ha-domain-icon")
|
||||
export class HaDomainIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public domain?: string;
|
||||
|
||||
@property({ attribute: false }) public deviceClass?: string;
|
||||
@@ -25,6 +29,22 @@ export class HaDomainIcon extends LitElement {
|
||||
@property({ attribute: "brand-fallback", type: Boolean })
|
||||
public brandFallback?: boolean;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: themesContext, subscribe: true })
|
||||
private _themes?: ContextType<typeof themesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: authContext, subscribe: true })
|
||||
private _auth?: ContextType<typeof authContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -34,12 +54,13 @@ export class HaDomainIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._hassConfig) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = domainIcon(
|
||||
this.hass,
|
||||
this._connection,
|
||||
this._hassConfig,
|
||||
this.domain,
|
||||
this.deviceClass,
|
||||
this.state
|
||||
@@ -65,9 +86,9 @@ export class HaDomainIcon extends LitElement {
|
||||
{
|
||||
domain: this.domain!,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
darkOptimized: this._themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
this._auth?.data.hassUrl
|
||||
);
|
||||
return html`
|
||||
<img
|
||||
|
||||
@@ -100,6 +100,9 @@ export class HaDropdown extends Dropdown {
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
wa-popup::part(popup) {
|
||||
z-index: 200;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -101,7 +101,11 @@ export class HaFilterDevices extends LitElement {
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceNameDisplay(device, this.hass)}
|
||||
${computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
@@ -151,14 +155,18 @@ export class HaFilterDevices extends LitElement {
|
||||
.filter(
|
||||
(device) =>
|
||||
!filter ||
|
||||
computeDeviceNameDisplay(device, this.hass)
|
||||
computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeDeviceNameDisplay(a, this.hass),
|
||||
computeDeviceNameDisplay(b, this.hass),
|
||||
computeDeviceNameDisplay(a, this.hass.localize, this.hass.states),
|
||||
computeDeviceNameDisplay(b, this.hass.localize, this.hass.states),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
|
||||
@@ -72,7 +72,6 @@ export class HaFilterDomains extends LitElement {
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
@@ -98,17 +97,14 @@ export class HaFilterLabels extends LitElement {
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) => {
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`<ha-check-list-item
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
@@ -119,8 +115,7 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`;
|
||||
}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
@@ -253,10 +248,6 @@ export class HaFilterLabels extends LitElement {
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@@ -142,6 +142,19 @@ export const computeInitialHaFormData = (
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else if ("numeric_threshold" in selector) {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
data[field.name] =
|
||||
type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -100,7 +100,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
inputMode="numeric"
|
||||
.label=${this.label}
|
||||
.hint=${this.helper}
|
||||
.value=${this.data !== undefined ? this.data.toString() : ""}
|
||||
.value=${this.data !== undefined && this.data !== null
|
||||
? this.data.toString()
|
||||
: ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
|
||||
+108
-78
@@ -1,4 +1,4 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, LitElement, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -54,6 +54,7 @@ export class HaGauge extends LitElement {
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
}
|
||||
this._segment_label = this._getSegmentLabel();
|
||||
this._rescaleSvg();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +71,7 @@ export class HaGauge extends LitElement {
|
||||
}
|
||||
this._angle = getAngle(this.value, this.min, this.max);
|
||||
this._segment_label = this._getSegmentLabel();
|
||||
this._rescaleSvg();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -88,87 +90,91 @@ export class HaGauge extends LitElement {
|
||||
/>
|
||||
|
||||
|
||||
${
|
||||
this.levels
|
||||
? [...this.levels]
|
||||
.sort((a, b) => a.level - b.level)
|
||||
.map((level, i, arr) => {
|
||||
const startLevel = i === 0 ? this.min : arr[i].level;
|
||||
const endLevel = i + 1 < arr.length ? arr[i + 1].level : this.max;
|
||||
${
|
||||
this.levels
|
||||
? (() => {
|
||||
const sortedLevels = [...this.levels].sort(
|
||||
(a, b) => a.level - b.level
|
||||
);
|
||||
|
||||
const startAngle = getAngle(startLevel, this.min, this.max);
|
||||
const endAngle = getAngle(endLevel, this.min, this.max);
|
||||
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
||||
if (
|
||||
sortedLevels.length > 0 &&
|
||||
sortedLevels[0].level !== this.min
|
||||
) {
|
||||
sortedLevels.unshift({
|
||||
level: this.min,
|
||||
stroke: "var(--info-color)",
|
||||
});
|
||||
}
|
||||
|
||||
const x1 = -arcRadius * Math.cos((startAngle * Math.PI) / 180);
|
||||
const y1 = -arcRadius * Math.sin((startAngle * Math.PI) / 180);
|
||||
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
|
||||
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
|
||||
return sortedLevels.map((level, i, arr) => {
|
||||
const startLevel = level.level;
|
||||
const endLevel =
|
||||
i + 1 < arr.length ? arr[i + 1].level : this.max;
|
||||
|
||||
const firstSegment = i === 0;
|
||||
const lastSegment = i === arr.length - 1;
|
||||
const startAngle = getAngle(startLevel, this.min, this.max);
|
||||
const endAngle = getAngle(endLevel, this.min, this.max);
|
||||
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
||||
|
||||
const paths: TemplateResult[] = [];
|
||||
const x1 =
|
||||
-arcRadius * Math.cos((startAngle * Math.PI) / 180);
|
||||
const y1 =
|
||||
-arcRadius * Math.sin((startAngle * Math.PI) / 180);
|
||||
const x2 = -arcRadius * Math.cos((endAngle * Math.PI) / 180);
|
||||
const y2 = -arcRadius * Math.sin((endAngle * Math.PI) / 180);
|
||||
|
||||
if (firstSegment) {
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: round"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
} else if (lastSegment) {
|
||||
const offsetAngle = 0.5;
|
||||
const midAngle = endAngle - offsetAngle;
|
||||
const xm = -arcRadius * Math.cos((midAngle * Math.PI) / 180);
|
||||
const ym = -arcRadius * Math.sin((midAngle * Math.PI) / 180);
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === arr.length - 1;
|
||||
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}"
|
||||
/>
|
||||
`);
|
||||
if (isFirst) {
|
||||
return svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: round"
|
||||
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
} else {
|
||||
paths.push(svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
/>
|
||||
`);
|
||||
}
|
||||
if (isLast) {
|
||||
const offsetAngle = 0.5;
|
||||
const midAngle = endAngle - offsetAngle;
|
||||
const xm =
|
||||
-arcRadius * Math.cos((midAngle * Math.PI) / 180);
|
||||
const ym =
|
||||
-arcRadius * Math.sin((midAngle * Math.PI) / 180);
|
||||
|
||||
return paths;
|
||||
})
|
||||
: ""
|
||||
}
|
||||
return svg`
|
||||
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${xm} ${ym}" />
|
||||
<path class="level" stroke="${level.stroke}" style="stroke-linecap: butt"
|
||||
d="M ${xm} ${ym} A ${arcRadius} ${arcRadius} 0 0 1 ${x2} ${y2}" />
|
||||
`;
|
||||
}
|
||||
|
||||
return svg`
|
||||
<path
|
||||
class="level"
|
||||
stroke="${level.stroke}"
|
||||
style="stroke-linecap: butt"
|
||||
d="M ${x1} ${y1} A ${arcRadius} ${arcRadius} 0 ${largeArc} 1 ${x2} ${y2}"
|
||||
></path>
|
||||
`;
|
||||
});
|
||||
})()
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
this.needle
|
||||
? svg`
|
||||
<line
|
||||
class="needle"
|
||||
x1="-35.0"
|
||||
y1="0"
|
||||
x2="-45.0"
|
||||
y2="0"
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
/>
|
||||
<path
|
||||
class="needle"
|
||||
d="M -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 Z"
|
||||
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
/>
|
||||
`
|
||||
: svg`
|
||||
<path
|
||||
@@ -179,7 +185,8 @@ export class HaGauge extends LitElement {
|
||||
/>
|
||||
`
|
||||
}
|
||||
|
||||
</svg>
|
||||
<svg class="text">
|
||||
<text
|
||||
class="value-text"
|
||||
x="0"
|
||||
@@ -204,6 +211,18 @@ export class HaGauge extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _rescaleSvg() {
|
||||
// Set the viewbox of the SVG containing the value to perfectly
|
||||
// fit the text
|
||||
// That way it will auto-scale correctly
|
||||
const svgRoot = this.shadowRoot!.querySelector(".text")!;
|
||||
const box = svgRoot.querySelector("text")!.getBBox()!;
|
||||
svgRoot.setAttribute(
|
||||
"viewBox",
|
||||
`${box.x} ${box.y} ${box.width} ${box.height}`
|
||||
);
|
||||
}
|
||||
|
||||
private _getSegmentLabel() {
|
||||
if (this.levels) {
|
||||
[...this.levels].sort((a, b) => a.level - b.level);
|
||||
@@ -224,32 +243,43 @@ export class HaGauge extends LitElement {
|
||||
.levels-base {
|
||||
fill: none;
|
||||
stroke: var(--primary-background-color);
|
||||
stroke-width: 8;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.value {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 6;
|
||||
stroke: var(--gauge-color);
|
||||
stroke-linecap: round;
|
||||
stroke-linecap: butt;
|
||||
transition: stroke-dashoffset 1s ease 0s;
|
||||
}
|
||||
|
||||
.needle {
|
||||
stroke: var(--primary-text-color);
|
||||
stroke-width: 2;
|
||||
fill: var(--primary-text-color);
|
||||
stroke: var(--card-background-color);
|
||||
color: var(--primary-text-color);
|
||||
stroke-width: 1;
|
||||
stroke-linecap: round;
|
||||
transform-origin: 0 0;
|
||||
transition: all 1s ease 0s;
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
max-height: 40%;
|
||||
max-width: 55%;
|
||||
left: 50%;
|
||||
bottom: 10%;
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
|
||||
.value-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
fill: var(--primary-text-color);
|
||||
|
||||
@@ -140,7 +140,7 @@ class HaHLSPlayer extends LitElement {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
|
||||
if (!isComponentLoaded(this.hass!, "stream")) {
|
||||
if (!isComponentLoaded(this.hass.config, "stream")) {
|
||||
this._setFatalError("Streaming component is not loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { getContrastedColorHex } from "../common/color/rgb";
|
||||
import { uid } from "../common/util/uid";
|
||||
import "./ha-tooltip";
|
||||
|
||||
/**
|
||||
* Returns CSS styles for a label's background & icon/text
|
||||
* @param color Label color defined in HEX format
|
||||
* @returns CSS styles
|
||||
*/
|
||||
export const getLabelColorStyle = (labelColor: string | undefined | null) => {
|
||||
const color = labelColor ? computeCssColor(labelColor) : undefined;
|
||||
return color
|
||||
? `--ha-label-background-color: ${color};
|
||||
--primary-text-color: ${getContrastedColorHex(labelColor!)};`
|
||||
: `--ha-label-background-color: rgba(var(--rgb-primary-text-color), 0.15);`;
|
||||
};
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property()
|
||||
public color?: string;
|
||||
|
||||
@property({ attribute: "description" })
|
||||
public description?: string;
|
||||
|
||||
private _elementId = "label-" + uid();
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!changedProps.has("color")) {
|
||||
return;
|
||||
}
|
||||
this.style.cssText = getLabelColorStyle(this.color);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip
|
||||
@@ -36,10 +62,6 @@ class HaLabel extends LitElement {
|
||||
:host {
|
||||
--ha-label-text-color: var(--primary-text-color);
|
||||
--ha-label-icon-color: var(--primary-text-color);
|
||||
--ha-label-background-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-label-background-opacity: 1;
|
||||
border: 1px solid var(--outline-color);
|
||||
position: relative;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
@@ -17,6 +16,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { getLabelColorStyle } from "./ha-label";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import "./ha-tooltip";
|
||||
@@ -106,9 +106,14 @@ export class HaLabelsPicker extends LitElement {
|
||||
labels?.find((label) => label.label_id === id) || {
|
||||
label_id: id,
|
||||
name: id,
|
||||
color: "rgba(var(--rgb-primary-text-color), 0.15)",
|
||||
}
|
||||
)
|
||||
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
|
||||
.map((label) => ({
|
||||
...label,
|
||||
style: getLabelColorStyle(label.color),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -135,9 +140,6 @@ export class HaLabelsPicker extends LitElement {
|
||||
(label) => label?.label_id,
|
||||
(label) => {
|
||||
if (!label) return nothing;
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
const elementId = "label-" + label.label_id;
|
||||
return html`
|
||||
<ha-tooltip
|
||||
@@ -154,7 +156,7 @@ export class HaLabelsPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${label.name}
|
||||
selected
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
style=${label.style}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
@@ -239,8 +241,10 @@ export class HaLabelsPicker extends LitElement {
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-container-color: var(
|
||||
--ha-label-background-color,
|
||||
var(--grey-color)
|
||||
);
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
label {
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@@ -129,7 +129,7 @@ class HaMountPicker extends LitElement {
|
||||
|
||||
private async _getMounts() {
|
||||
try {
|
||||
if (isComponentLoaded(this.hass, "hassio")) {
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
this._mounts = await fetchSupervisorMounts(this.hass);
|
||||
if (this.usage === SupervisorMountUsage.BACKUP && !this.value) {
|
||||
this.value = this._mounts.default_backup_mount || _BACKUP_DATA_DISK_;
|
||||
|
||||
@@ -132,7 +132,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
@@ -158,7 +157,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
? html`
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${item.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
|
||||
@@ -798,11 +798,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-3);
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-combo-box-item {
|
||||
@@ -873,12 +873,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-3) var(--ha-space-3);
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-3);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) .sections {
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
.sections ha-filter-chip {
|
||||
@@ -915,10 +915,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.section-title-wrapper {
|
||||
height: 0;
|
||||
position: relative;
|
||||
|
||||
@@ -121,6 +121,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
--md-list-item-disabled-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
@@ -141,13 +143,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
@@ -158,10 +153,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
|
||||
@@ -2,6 +2,7 @@ import memoizeOne from "memoize-one";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { mdiChartBellCurveCumulative } from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
NumericThresholdSelector,
|
||||
@@ -76,6 +77,18 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("value") || changedProperties.has("selector")) &&
|
||||
!this.value
|
||||
) {
|
||||
const mode = this._getMode();
|
||||
const type = DEFAULT_TYPE[mode];
|
||||
fireEvent(this, "value-changed", { value: { type } });
|
||||
}
|
||||
}
|
||||
|
||||
private _getUnitOptions() {
|
||||
return this.selector.numeric_threshold?.unit_of_measurement;
|
||||
}
|
||||
@@ -220,6 +233,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
return [
|
||||
{
|
||||
value: "any",
|
||||
iconPath: mdiChartBellCurveCumulative,
|
||||
label: localize(
|
||||
"ui.components.selectors.numeric_threshold.changed.any"
|
||||
),
|
||||
@@ -273,7 +287,9 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
const numberSelector = {
|
||||
number: {
|
||||
...this.selector.numeric_threshold?.number,
|
||||
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
|
||||
...(!showUnit && effectiveUnit
|
||||
? { unit_of_measurement: effectiveUnit }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const entitySelector = {
|
||||
@@ -481,7 +497,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
.value-inputs {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { PeriodKey, PeriodSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
const PERIODS = {
|
||||
none: undefined,
|
||||
today: { calendar: { period: "day" } },
|
||||
yesterday: { calendar: { period: "day", offset: -1 } },
|
||||
tomorrow: { calendar: { period: "day", offset: 1 } },
|
||||
this_week: { calendar: { period: "week" } },
|
||||
last_week: { calendar: { period: "week", offset: -1 } },
|
||||
next_week: { calendar: { period: "week", offset: 1 } },
|
||||
this_month: { calendar: { period: "month" } },
|
||||
last_month: { calendar: { period: "month", offset: -1 } },
|
||||
next_month: { calendar: { period: "month", offset: 1 } },
|
||||
this_year: { calendar: { period: "year" } },
|
||||
last_year: { calendar: { period: "year", offset: -1 } },
|
||||
next_7d: { calendar: { period: "day", offset: 7 } },
|
||||
next_30d: { calendar: { period: "day", offset: 30 } },
|
||||
} as const;
|
||||
|
||||
@customElement("ha-selector-period")
|
||||
export class HaPeriodSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: PeriodSelector;
|
||||
|
||||
@property({ attribute: false }) public value?: unknown;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
selectedPeriodKey: PeriodKey | undefined,
|
||||
selector: PeriodSelector,
|
||||
localize: LocalizeFunc
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "period",
|
||||
required: this.required,
|
||||
selector:
|
||||
selectedPeriodKey && selectedPeriodKey in this._periods(selector)
|
||||
? {
|
||||
select: {
|
||||
multiple: false,
|
||||
options: Object.keys(this._periods(selector)).map(
|
||||
(periodKey) => ({
|
||||
value: periodKey,
|
||||
label:
|
||||
localize(
|
||||
`ui.components.selectors.period.periods.${periodKey as PeriodKey}`
|
||||
) || periodKey,
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
: { object: {} },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = this._data(this.value, this.selector);
|
||||
|
||||
const schema = this._schema(
|
||||
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
|
||||
this.selector,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _periods = memoizeOne((selector: PeriodSelector) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(PERIODS).filter(([key]) =>
|
||||
selector.period?.options?.includes(key as any)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private _data = memoizeOne((value: unknown, selector: PeriodSelector) => {
|
||||
for (const [periodKey, period] of Object.entries(this._periods(selector))) {
|
||||
if (deepEqual(period, value)) {
|
||||
return { period: periodKey };
|
||||
}
|
||||
}
|
||||
return { period: value };
|
||||
});
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (typeof newValue.period === "string") {
|
||||
const periods = this._periods(this.selector);
|
||||
if (newValue.period in periods) {
|
||||
const period = this._periods(this.selector)[newValue.period];
|
||||
fireEvent(this, "value-changed", { value: period });
|
||||
}
|
||||
} else {
|
||||
fireEvent(this, "value-changed", { value: newValue.period });
|
||||
}
|
||||
}
|
||||
|
||||
private _computeHelperCallback = () => this.helper;
|
||||
|
||||
private _computeLabelCallback = () => this.label;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-period": HaPeriodSelector;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ const LOAD_ELEMENTS = {
|
||||
number: () => import("./ha-selector-number"),
|
||||
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
|
||||
object: () => import("./ha-selector-object"),
|
||||
period: () => import("./ha-selector-period"),
|
||||
qr_code: () => import("./ha-selector-qr-code"),
|
||||
select: () => import("./ha-selector-select"),
|
||||
selector: () => import("./ha-selector-selector"),
|
||||
|
||||
@@ -516,17 +516,10 @@ export class HaServiceControl extends LitElement {
|
||||
`}
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.components.service-control.target")}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-settings-row")
|
||||
@@ -16,17 +17,28 @@ export class HaSettingsRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public empty = false;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"description"
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hasDescription = this._hasSlotController.test("description");
|
||||
|
||||
return html`
|
||||
<div class="prefix-wrap">
|
||||
<slot name="prefix"></slot>
|
||||
<div
|
||||
class="body"
|
||||
?two-line=${!this.threeLine}
|
||||
?two-line=${!this.threeLine && hasDescription}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div class="secondary"><slot name="description"></slot></div>
|
||||
${hasDescription
|
||||
? html`<span class="secondary"
|
||||
><slot name="description"></slot
|
||||
></span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, css, LitElement, nothing } from "lit";
|
||||
import { mdiStarFourPoints } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
|
||||
import { customElement, state, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type {
|
||||
AITaskPreferences,
|
||||
GenDataTask,
|
||||
GenDataTaskResult,
|
||||
} from "../data/ai_task";
|
||||
import { fetchAITaskPreferences, generateDataAITask } from "../data/ai_task";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./chips/ha-assist-chip";
|
||||
import "./ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -56,7 +56,7 @@ export class HaSuggestWithAIButton extends LitElement {
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
|
||||
if (!this.hass || !isComponentLoaded(this.hass.config, "ai_task")) {
|
||||
return;
|
||||
}
|
||||
fetchAITaskPreferences(this.hass).then((prefs) => {
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
/**
|
||||
* Legacy wrapper around ha-input that preserves the mwc-textfield API.
|
||||
* New code should use ha-input directly.
|
||||
* @deprecated Use ha-input instead.
|
||||
*/
|
||||
@customElement("ha-textfield")
|
||||
export class HaTextField extends LitElement {
|
||||
@property({ type: String })
|
||||
public value = "";
|
||||
|
||||
@property({ type: String })
|
||||
public type:
|
||||
| "text"
|
||||
| "search"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "email"
|
||||
| "password"
|
||||
| "date"
|
||||
| "month"
|
||||
| "week"
|
||||
| "time"
|
||||
| "datetime-local"
|
||||
| "number"
|
||||
| "color" = "text";
|
||||
|
||||
@property({ type: String })
|
||||
public label = "";
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder = "";
|
||||
|
||||
@property({ type: String })
|
||||
public prefix = "";
|
||||
|
||||
@property({ type: String })
|
||||
public suffix = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
public icon = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line lit/attribute-names
|
||||
public iconTrailing = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
@property({ type: Number, attribute: "minlength" })
|
||||
public minLength = -1;
|
||||
|
||||
@property({ type: Number, attribute: "maxlength" })
|
||||
public maxLength = -1;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public outlined = false;
|
||||
|
||||
@property({ type: String })
|
||||
public helper = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "validateoninitialrender" })
|
||||
public validateOnInitialRender = false;
|
||||
|
||||
@property({ type: String, attribute: "validationmessage" })
|
||||
public validationMessage = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "autovalidate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: String })
|
||||
public pattern = "";
|
||||
|
||||
@property()
|
||||
public min: number | string = "";
|
||||
|
||||
@property()
|
||||
public max: number | string = "";
|
||||
|
||||
@property()
|
||||
public step: number | "any" | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
public size: number | null = null;
|
||||
|
||||
@property({ type: Boolean, attribute: "helperpersistent" })
|
||||
public helperPersistent = false;
|
||||
|
||||
@property({ attribute: "charcounter" })
|
||||
public charCounter: boolean | "external" | "internal" = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "endaligned" })
|
||||
public endAligned = false;
|
||||
|
||||
@property({ type: String, attribute: "inputmode" })
|
||||
public inputMode = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "readonly" })
|
||||
public readOnly = false;
|
||||
|
||||
@property({ type: String })
|
||||
public name = "";
|
||||
|
||||
@property({ type: String })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize = "";
|
||||
|
||||
// --- ha-textfield-specific properties ---
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" })
|
||||
public errorMessage?: string;
|
||||
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
||||
@query("ha-input")
|
||||
private _haInput?: HaInput;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public get formElement(): HTMLInputElement | undefined {
|
||||
return (this._haInput as any)?._input?.input;
|
||||
}
|
||||
|
||||
public select(): void {
|
||||
this._haInput?.select();
|
||||
}
|
||||
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._haInput?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._haInput?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._haInput?.checkValidity() ?? true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._haInput?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
public setCustomValidity(message: string): void {
|
||||
this.validationMessage = message;
|
||||
this.invalid = !!message;
|
||||
}
|
||||
|
||||
/** No-op. Preserved for backward compatibility. */
|
||||
public layout(): void {
|
||||
// no-op — mwc-textfield needed this for notched outline recalculation
|
||||
}
|
||||
|
||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.validateOnInitialRender) {
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("invalid") && this._haInput) {
|
||||
if (
|
||||
this.invalid ||
|
||||
(changedProperties.get("invalid") !== undefined && !this.invalid)
|
||||
) {
|
||||
this.reportValidity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override render(): TemplateResult {
|
||||
const errorMsg = this.errorMessage || this.validationMessage;
|
||||
return html`
|
||||
<ha-input
|
||||
.type=${this.type}
|
||||
.value=${this.value || undefined}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.readonly=${this.readOnly}
|
||||
.pattern=${this.pattern || undefined}
|
||||
.minlength=${this.minLength > 0 ? this.minLength : undefined}
|
||||
.maxlength=${this.maxLength > 0 ? this.maxLength : undefined}
|
||||
.min=${this.min !== "" ? this.min : undefined}
|
||||
.max=${this.max !== "" ? this.max : undefined}
|
||||
.step=${this.step ?? undefined}
|
||||
.name=${this.name || undefined}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.autocorrect=${this.autocorrect}
|
||||
.spellcheck=${this.inputSpellcheck === "true"}
|
||||
.inputmode=${this.inputMode}
|
||||
.autocapitalize=${this.autocapitalize || ""}
|
||||
.invalid=${this.invalid}
|
||||
.validationMessage=${errorMsg || ""}
|
||||
.autoValidate=${this.autoValidate}
|
||||
.hint=${this.helper}
|
||||
.withoutSpinButtons=${this.type === "number"}
|
||||
.insetLabel=${this.prefix}
|
||||
@input=${this._onInput}
|
||||
@change=${this._onChange}
|
||||
>
|
||||
${this.icon
|
||||
? html`<slot name="leadingIcon" slot="start"></slot>`
|
||||
: nothing}
|
||||
${this.prefix
|
||||
? html`<span class="prefix" slot="start">${this.prefix}</span>`
|
||||
: nothing}
|
||||
${this.suffix
|
||||
? html`<span class="suffix" slot="end">${this.suffix}</span>`
|
||||
: nothing}
|
||||
${this.iconTrailing
|
||||
? html`<slot name="trailingIcon" slot="end"></slot>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onInput(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
this.value = this._haInput?.value ?? "";
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
ha-input {
|
||||
--ha-input-padding-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
padding-top: var(--ha-space-3);
|
||||
margin-inline-end: var(--text-field-prefix-padding-right);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-textfield": HaTextField;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-second" })
|
||||
public enableSecond = false;
|
||||
|
||||
@@ -71,6 +73,7 @@ export class HaTimeInput extends LitElement {
|
||||
.clearable=${this.clearable && this.value !== undefined}
|
||||
.helper=${this.helper}
|
||||
.placeholderLabels=${this.placeholderLabels}
|
||||
.autoValidate=${this.autoValidate}
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
@@ -86,6 +89,7 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
let value: string | undefined;
|
||||
let updateHours = 0;
|
||||
|
||||
// An undefined eventValue means the time selector is being cleared,
|
||||
// the `value` variable will (intentionally) be left undefined.
|
||||
@@ -97,6 +101,8 @@ export class HaTimeInput extends LitElement {
|
||||
) {
|
||||
let hours = eventValue.hours || 0;
|
||||
if (eventValue && useAMPM) {
|
||||
updateHours =
|
||||
hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0;
|
||||
if (eventValue.amPm === "PM" && hours < 12) {
|
||||
hours += 12;
|
||||
}
|
||||
@@ -115,6 +121,17 @@ export class HaTimeInput extends LitElement {
|
||||
}`;
|
||||
}
|
||||
|
||||
if (updateHours) {
|
||||
// If the user entered a 24hr time in a 12hr input, we need to refresh the
|
||||
// input to ensure it resets back to the 12hr equivalent.
|
||||
this.updateComplete.then(() => {
|
||||
const input = this._input;
|
||||
if (input) {
|
||||
input.hours = updateHours;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
+91
-67
@@ -1,9 +1,15 @@
|
||||
import "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import type WaPopup from "@home-assistant/webawesome/dist/components/popup/popup";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { popoverSupported } from "../common/feature-detect/support-popover";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
|
||||
export type ToastCloseReason =
|
||||
@@ -22,12 +28,15 @@ export class HaToast extends LitElement {
|
||||
|
||||
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
|
||||
|
||||
@query("wa-popup")
|
||||
private _popup?: WaPopup;
|
||||
|
||||
@query(".toast")
|
||||
private _toast?: HTMLDivElement;
|
||||
|
||||
@queryAssignedElements({ slot: "action", flatten: true })
|
||||
private _actionElements?: Element[];
|
||||
|
||||
@queryAssignedElements({ slot: "dismiss", flatten: true })
|
||||
private _dismissElements?: Element[];
|
||||
|
||||
@state() private _active = false;
|
||||
|
||||
@state() private _visible = false;
|
||||
@@ -48,7 +57,6 @@ export class HaToast extends LitElement {
|
||||
clearTimeout(this._dismissTimer);
|
||||
|
||||
if (this._active && this._visible) {
|
||||
this._popup?.reposition();
|
||||
this._setDismissTimer();
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +70,7 @@ export class HaToast extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._popup?.reposition();
|
||||
this._showToastPopover();
|
||||
await nextRender();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
@@ -102,6 +110,7 @@ export class HaToast extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hideToastPopover();
|
||||
this._active = false;
|
||||
await this.updateComplete;
|
||||
|
||||
@@ -123,6 +132,34 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _isPopoverOpen(): boolean {
|
||||
if (!this._toast || !popoverSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this._toast.matches(":popover-open");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _showToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.showPopover?.();
|
||||
}
|
||||
|
||||
private _hideToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.hidePopover?.();
|
||||
}
|
||||
|
||||
private async _waitForTransitionEnd(): Promise<void> {
|
||||
const toastEl = this._toast;
|
||||
if (!toastEl) {
|
||||
@@ -138,82 +175,70 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasAction =
|
||||
(this._actionElements?.length ?? 0) > 0 ||
|
||||
(this._dismissElements?.length ?? 0) > 0;
|
||||
|
||||
return html`
|
||||
<wa-popup
|
||||
placement="top"
|
||||
.active=${this._active}
|
||||
.distance=${16}
|
||||
skidding="0"
|
||||
flip
|
||||
shift
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
active: this._active,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<div id="toast-anchor" slot="anchor" aria-hidden="true"></div>
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class=${classMap({ actions: true, "has-action": hasAction })}>
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
</wa-popup>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
#toast-anchor {
|
||||
position: fixed;
|
||||
bottom: calc(var(--ha-space-2) + var(--safe-area-inset-bottom));
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
wa-popup::part(popup) {
|
||||
padding: 0;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toast {
|
||||
box-sizing: border-box;
|
||||
min-width: min(
|
||||
350px,
|
||||
calc(
|
||||
100vw - var(--ha-space-4) - var(--safe-area-inset-left) - var(
|
||||
--safe-area-inset-right
|
||||
)
|
||||
)
|
||||
position: fixed;
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4)
|
||||
);
|
||||
max-width: 650px;
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
width: max-content;
|
||||
height: auto;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
|
||||
max-width: min(650px, var(--safe-width));
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
background-color: var(--ha-color-neutral-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
opacity: 0;
|
||||
transform: translateY(var(--ha-space-2));
|
||||
transform: translate(-50%, var(--ha-space-2));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease,
|
||||
transform var(--ha-animation-duration-fast, 150ms) ease;
|
||||
}
|
||||
|
||||
.toast:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -228,15 +253,14 @@ export class HaToast extends LitElement {
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
wa-popup::part(popup) {
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.actions:not(.has-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.toast {
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
);
|
||||
min-width: var(--safe-width);
|
||||
max-width: var(--safe-width);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
export type InputType =
|
||||
| "date"
|
||||
@@ -235,7 +236,8 @@ export class HaInput extends LitElement {
|
||||
this,
|
||||
"label",
|
||||
"hint",
|
||||
"input"
|
||||
"input",
|
||||
"start"
|
||||
);
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
@@ -287,7 +289,9 @@ export class HaInput extends LitElement {
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return this._input?.checkValidity() ?? true;
|
||||
return nativeElementInternalsSupported
|
||||
? (this._input?.checkValidity() ?? true)
|
||||
: true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
@@ -318,6 +322,8 @@ export class HaInput extends LitElement {
|
||||
? false
|
||||
: this._hasSlotController.test("hint");
|
||||
|
||||
const hasStartSlot = this._hasSlotController.test("start");
|
||||
|
||||
return html`
|
||||
<wa-input
|
||||
.type=${this.type}
|
||||
@@ -348,7 +354,8 @@ export class HaInput extends LitElement {
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
(this.label && this.placeholder),
|
||||
(this.label && this.placeholder) ||
|
||||
(hasStartSlot && this.insetLabel),
|
||||
"no-label": !this.label,
|
||||
"hint-hidden":
|
||||
!this.hint &&
|
||||
@@ -589,6 +596,7 @@ export class HaInput extends LitElement {
|
||||
}
|
||||
:host([type="color"]) wa-input::part(input) {
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(input) {
|
||||
@@ -620,8 +628,12 @@ export class HaInput extends LitElement {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
height: var(--ha-space-5);
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
@@ -632,6 +644,7 @@ export class HaInput extends LitElement {
|
||||
|
||||
wa-input.hint-hidden::part(hint) {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { animate } from "@lit-labs/motion";
|
||||
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
mdiCheckboxBlankOutline,
|
||||
mdiCheckboxMarkedOutline,
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -26,8 +26,8 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-check-list-item";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-spinner";
|
||||
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
|
||||
)}
|
||||
</ha-list>
|
||||
`}
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
${isComponentLoaded(this.hass.config, "hassio")
|
||||
? html`<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.tip_media_storage",
|
||||
|
||||
@@ -524,7 +524,13 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
|
||||
return {
|
||||
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
|
||||
name: device
|
||||
? computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
: item,
|
||||
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
|
||||
fallbackIconPath: mdiDevices,
|
||||
notFound: !device,
|
||||
@@ -535,7 +541,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities)
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device } = stateObject
|
||||
? getEntityContext(
|
||||
|
||||
@@ -161,7 +161,13 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
}
|
||||
|
||||
return {
|
||||
name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
|
||||
name: device
|
||||
? computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
)
|
||||
: itemId,
|
||||
fallbackIconPath: mdiDevices,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/skeleton/skeleton";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@@ -12,6 +13,8 @@ import { customElement, property } from "lit/decorators";
|
||||
* @slot primary - The primary text container.
|
||||
* @slot secondary - The secondary text container.
|
||||
*
|
||||
* @property {boolean} secondaryLoading - Whether the secondary text is loading. Shows a skeleton placeholder.
|
||||
*
|
||||
* @cssprop --ha-tile-info-primary-font-size - The font size of the primary text. defaults to `var(--ha-font-size-m)`.
|
||||
* @cssprop --ha-tile-info-primary-font-weight - The font weight of the primary text. defaults to `var(--ha-font-weight-medium)`.
|
||||
* @cssprop --ha-tile-info-primary-line-height - The line height of the primary text. defaults to `var(--ha-line-height-normal)`.
|
||||
@@ -29,21 +32,31 @@ export class HaTileInfo extends LitElement {
|
||||
|
||||
@property() public secondary?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "secondary-loading" })
|
||||
public secondaryLoading = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="info">
|
||||
<slot name="primary" class="primary">
|
||||
<span>${this.primary}</span>
|
||||
</slot>
|
||||
<slot name="secondary" class="secondary">
|
||||
<span>${this.secondary}</span>
|
||||
</slot>
|
||||
${this.secondaryLoading
|
||||
? html`<div class="secondary">
|
||||
<wa-skeleton class="placeholder" effect="pulse"></wa-skeleton>
|
||||
</div>`
|
||||
: html`<slot name="secondary" class="secondary">
|
||||
<span>${this.secondary}</span>
|
||||
</slot>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
--tile-info-primary-font-size: var(
|
||||
--ha-tile-info-primary-font-size,
|
||||
var(--ha-font-size-m)
|
||||
@@ -112,6 +125,15 @@ export class HaTileInfo extends LitElement {
|
||||
line-height: var(--tile-info-secondary-line-height);
|
||||
letter-spacing: var(--tile-info-secondary-letter-spacing);
|
||||
color: var(--tile-info-secondary-color);
|
||||
width: 100%;
|
||||
}
|
||||
.placeholder {
|
||||
width: 140px;
|
||||
max-width: 100%;
|
||||
height: var(--tile-info-secondary-font-size);
|
||||
--wa-border-radius-pill: var(--ha-border-radius-sm);
|
||||
--color: var(--ha-color-fill-neutral-normal-resting);
|
||||
--sheen-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export class HaTraceLogbook extends LitElement {
|
||||
></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
No Logbook entries found for this step.
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.trace.path.no_logbook_entries"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
+26
-13
@@ -78,6 +78,19 @@ const localizeTimeString = (
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumericLimitValue = (
|
||||
hass: HomeAssistant,
|
||||
value?: number | string
|
||||
) => {
|
||||
if (typeof value !== "string" || !isValidEntityId(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return hass.states[value]
|
||||
? computeStateName(hass.states[value]) || value
|
||||
: value;
|
||||
};
|
||||
|
||||
export const describeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
@@ -233,8 +246,8 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: trigger.above,
|
||||
below: trigger.below,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -246,7 +259,7 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: trigger.above,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -258,7 +271,7 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
below: trigger.below,
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -813,16 +826,16 @@ const describeLegacyTrigger = (
|
||||
: trigger.entity_id;
|
||||
|
||||
let offsetChoice = "other";
|
||||
let offset: string | string[] = "";
|
||||
let offset = "";
|
||||
if (trigger.offset) {
|
||||
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||
offset = trigger.offset.startsWith("-")
|
||||
const parts = trigger.offset.startsWith("-")
|
||||
? trigger.offset.substring(1).split(":")
|
||||
: trigger.offset.split(":");
|
||||
const duration = {
|
||||
hours: offset.length > 0 ? +offset[0] : 0,
|
||||
minutes: offset.length > 1 ? +offset[1] : 0,
|
||||
seconds: offset.length > 2 ? +offset[2] : 0,
|
||||
hours: parts.length > 0 ? +parts[0] : 0,
|
||||
minutes: parts.length > 1 ? +parts[1] : 0,
|
||||
seconds: parts.length > 2 ? +parts[2] : 0,
|
||||
};
|
||||
offset = formatDurationLong(hass.locale, duration);
|
||||
if (offset === "") {
|
||||
@@ -1116,8 +1129,8 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
below: condition.below,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1128,7 +1141,7 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1139,7 +1152,7 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
below: condition.below,
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getDeviceArea } from "../../common/entity/context/get_device_context";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
@@ -144,11 +144,12 @@ export const getDevices = (
|
||||
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
device,
|
||||
hass,
|
||||
hass.localize,
|
||||
hass.states,
|
||||
deviceEntityLookup[device.id]
|
||||
);
|
||||
|
||||
const { area } = getDeviceContext(device, hass);
|
||||
const area = getDeviceArea(device, hass.areas);
|
||||
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const fetchErrorLog = (hass: HomeAssistant) =>
|
||||
hass.callApi<string>("GET", "error_log");
|
||||
|
||||
export const getErrorLogDownloadUrl = (hass: HomeAssistant) =>
|
||||
isComponentLoaded(hass, "hassio") &&
|
||||
isComponentLoaded(hass.config, "hassio") &&
|
||||
atLeastVersion(hass.config.version, 2025, 10)
|
||||
? "/api/hassio/core/logs/latest"
|
||||
: "/api/error_log";
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface CoreFrontendSystemData {
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
welcome_banner_dismissed?: boolean;
|
||||
hidden_summaries?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -15,7 +15,7 @@ interface HassioHardwareAudioList {
|
||||
};
|
||||
}
|
||||
|
||||
interface HardwareDevice {
|
||||
export interface HardwareDevice {
|
||||
attributes: Record<string, string>;
|
||||
by_id: null | string;
|
||||
dev_path: string;
|
||||
|
||||
+77
-30
@@ -3,8 +3,10 @@ import {
|
||||
mdiAirFilter,
|
||||
mdiAlert,
|
||||
mdiAppleSafari,
|
||||
mdiBattery,
|
||||
mdiBell,
|
||||
mdiBookmark,
|
||||
mdiBrightness6,
|
||||
mdiBullhorn,
|
||||
mdiButtonPointer,
|
||||
mdiCalendar,
|
||||
@@ -16,13 +18,18 @@ import {
|
||||
mdiCog,
|
||||
mdiCommentAlert,
|
||||
mdiCounter,
|
||||
mdiDoorOpen,
|
||||
mdiEye,
|
||||
mdiFlash,
|
||||
mdiFlower,
|
||||
mdiFormatListBulleted,
|
||||
mdiFormTextbox,
|
||||
mdiForumOutline,
|
||||
mdiGarageOpen,
|
||||
mdiGate,
|
||||
mdiGoogleAssistant,
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiHomeAccount,
|
||||
mdiHomeAutomation,
|
||||
mdiImage,
|
||||
mdiImageFilterFrames,
|
||||
@@ -30,6 +37,7 @@ import {
|
||||
mdiLightbulb,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMicrophoneMessage,
|
||||
mdiMotionSensor,
|
||||
mdiPalette,
|
||||
mdiRayVertex,
|
||||
mdiRemote,
|
||||
@@ -40,13 +48,17 @@ import {
|
||||
mdiScriptText,
|
||||
mdiSpeakerMessage,
|
||||
mdiStarFourPoints,
|
||||
mdiThermometer,
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
mdiToggleSwitch,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWhiteBalanceSunny,
|
||||
mdiWindowClosed,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { Connection, HassEntity } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { atLeastVersion } from "../common/config/version";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
@@ -60,6 +72,7 @@ import type {
|
||||
} from "./entity/entity_registry";
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { callWS } from "../util/websocket";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||
|
||||
@@ -75,6 +88,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
air_quality: mdiAirFilter,
|
||||
alert: mdiAlert,
|
||||
automation: mdiRobot,
|
||||
battery: mdiBattery,
|
||||
calendar: mdiCalendar,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
@@ -84,10 +98,15 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
datetime: mdiCalendarClock,
|
||||
demo: mdiHomeAssistant,
|
||||
device_tracker: mdiAccount,
|
||||
door: mdiDoorOpen,
|
||||
garage_door: mdiGarageOpen,
|
||||
gate: mdiGate,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
homekit: mdiHomeAutomation,
|
||||
humidity: mdiWaterPercent,
|
||||
illuminance: mdiBrightness6,
|
||||
image_processing: mdiImageFilterFrames,
|
||||
image: mdiImage,
|
||||
infrared: mdiLedOn,
|
||||
@@ -99,11 +118,15 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
input_text: mdiFormTextbox,
|
||||
lawn_mower: mdiRobotMower,
|
||||
light: mdiLightbulb,
|
||||
moisture: mdiWater,
|
||||
motion: mdiMotionSensor,
|
||||
notify: mdiCommentAlert,
|
||||
number: mdiRayVertex,
|
||||
occupancy: mdiHomeAccount,
|
||||
persistent_notification: mdiBell,
|
||||
person: mdiAccount,
|
||||
plant: mdiFlower,
|
||||
power: mdiFlash,
|
||||
proximity: mdiAppleSafari,
|
||||
remote: mdiRemote,
|
||||
scene: mdiPalette,
|
||||
@@ -115,6 +138,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
siren: mdiBullhorn,
|
||||
stt: mdiMicrophoneMessage,
|
||||
sun: mdiWhiteBalanceSunny,
|
||||
temperature: mdiThermometer,
|
||||
text: mdiFormTextbox,
|
||||
time: mdiClock,
|
||||
timer: mdiTimerOutline,
|
||||
@@ -124,6 +148,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
vacuum: mdiRobotVacuum,
|
||||
wake_word: mdiChatSleep,
|
||||
weather: mdiWeatherPartlyCloudy,
|
||||
window: mdiWindowClosed,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
@@ -229,18 +254,19 @@ interface CategoryType {
|
||||
}
|
||||
|
||||
export const getHassIcons = async <T extends IconCategory>(
|
||||
hass: HomeAssistant,
|
||||
connection: Connection,
|
||||
category: T,
|
||||
integration?: string
|
||||
) =>
|
||||
hass.callWS<IconResources<CategoryType[T]>>({
|
||||
callWS<IconResources<CategoryType[T]>>(connection, {
|
||||
type: "frontend/get_icons",
|
||||
category,
|
||||
integration,
|
||||
});
|
||||
|
||||
export const getPlatformIcons = async (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
connection: Connection,
|
||||
integration: string,
|
||||
force = false
|
||||
): Promise<PlatformIcons | undefined> => {
|
||||
@@ -248,12 +274,12 @@ export const getPlatformIcons = async (
|
||||
return resources.entity[integration];
|
||||
}
|
||||
if (
|
||||
!isComponentLoaded(hass, integration) ||
|
||||
!atLeastVersion(hass.connection.haVersion, 2024, 2)
|
||||
!isComponentLoaded(hassConfig, integration) ||
|
||||
!atLeastVersion(connection.haVersion, 2024, 2)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const result = getHassIcons(hass, "entity", integration).then(
|
||||
const result = getHassIcons(connection, "entity", integration).then(
|
||||
(res) => res?.resources[integration]
|
||||
);
|
||||
resources.entity[integration] = result;
|
||||
@@ -261,15 +287,13 @@ export const getPlatformIcons = async (
|
||||
};
|
||||
|
||||
export const getComponentIcons = async (
|
||||
hass: HomeAssistant,
|
||||
connection: Connection,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
domain: string,
|
||||
force = false
|
||||
): Promise<ComponentIcons | undefined> => {
|
||||
// For Cast, old instances can connect to it.
|
||||
if (
|
||||
__BACKWARDS_COMPAT__ &&
|
||||
!atLeastVersion(hass.connection.haVersion, 2024, 2)
|
||||
) {
|
||||
if (__BACKWARDS_COMPAT__ && !atLeastVersion(connection.haVersion, 2024, 2)) {
|
||||
return import("../fake_data/entity_component_icons")
|
||||
.then((mod) => mod.ENTITY_COMPONENT_ICONS)
|
||||
.then((res) => res[domain]);
|
||||
@@ -283,12 +307,12 @@ export const getComponentIcons = async (
|
||||
return resources.entity_component.resources.then((res) => res[domain]);
|
||||
}
|
||||
|
||||
if (!isComponentLoaded(hass, domain)) {
|
||||
if (!isComponentLoaded(hassConfig, domain)) {
|
||||
return undefined;
|
||||
}
|
||||
resources.entity_component.domains = [...hass.config.components];
|
||||
resources.entity_component.domains = [...hassConfig.components];
|
||||
resources.entity_component.resources = getHassIcons(
|
||||
hass,
|
||||
connection,
|
||||
"entity_component"
|
||||
).then((result) => result.resources);
|
||||
return resources.entity_component.resources.then((res) => res[domain]);
|
||||
@@ -308,10 +332,12 @@ export const getCategoryIcons = async <
|
||||
Record<string, CategoryType[T]>
|
||||
>;
|
||||
}
|
||||
resources[category].all = getHassIcons(hass, category).then((res) => {
|
||||
resources[category].domains = res.resources as any;
|
||||
return res?.resources as Record<string, CategoryType[T]>;
|
||||
}) as any;
|
||||
resources[category].all = getHassIcons(hass.connection, category).then(
|
||||
(res) => {
|
||||
resources[category].domains = res.resources as any;
|
||||
return res?.resources as Record<string, CategoryType[T]>;
|
||||
}
|
||||
) as any;
|
||||
return resources[category].all as Promise<Record<string, CategoryType[T]>>;
|
||||
}
|
||||
if (!force && domain in resources[category].domains) {
|
||||
@@ -323,10 +349,10 @@ export const getCategoryIcons = async <
|
||||
return resources[category].domains[domain] as Promise<CategoryType[T]>;
|
||||
}
|
||||
}
|
||||
if (!isComponentLoaded(hass, domain)) {
|
||||
if (!isComponentLoaded(hass.config, domain)) {
|
||||
return undefined;
|
||||
}
|
||||
const result = getHassIcons(hass, category, domain);
|
||||
const result = getHassIcons(hass.connection, category, domain);
|
||||
resources[category].domains[domain] = result.then(
|
||||
(res) => res?.resources[domain]
|
||||
) as any;
|
||||
@@ -467,7 +493,11 @@ const getEntityIcon = async (
|
||||
|
||||
let icon: string | undefined;
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(hass, platform);
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
const translations = platformIcons[domain]?.[translation_key];
|
||||
|
||||
@@ -480,7 +510,11 @@ const getEntityIcon = async (
|
||||
}
|
||||
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
const translations =
|
||||
(device_class && entityComponentIcons[device_class]) ||
|
||||
@@ -511,7 +545,11 @@ export const attributeIcon = async (
|
||||
(state.attributes[attribute] as string | number | undefined);
|
||||
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(hass, platform);
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
icon = getIconFromTranslations(
|
||||
value,
|
||||
@@ -520,7 +558,11 @@ export const attributeIcon = async (
|
||||
}
|
||||
}
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
const translations =
|
||||
(deviceClass &&
|
||||
@@ -548,7 +590,7 @@ export const triggerIcon = async (
|
||||
icon = trgrIcon?.trigger;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
icon = await domainIcon(hass.connection, hass.config, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
@@ -567,7 +609,7 @@ export const conditionIcon = async (
|
||||
icon = condIcon?.condition;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
icon = await domainIcon(hass.connection, hass.config, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
@@ -585,7 +627,7 @@ export const serviceIcon = async (
|
||||
icon = srvceIcon?.service;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
icon = await domainIcon(hass.connection, hass.config, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
@@ -606,12 +648,17 @@ export const serviceSectionIcon = async (
|
||||
};
|
||||
|
||||
export const domainIcon = async (
|
||||
hass: HomeAssistant,
|
||||
connection: Connection,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
domain: string,
|
||||
deviceClass?: string,
|
||||
state?: string
|
||||
): Promise<string | undefined> => {
|
||||
const entityComponentIcons = await getComponentIcons(hass, domain);
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
connection,
|
||||
hassConfig,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
const translations =
|
||||
(deviceClass && entityComponentIcons[deviceClass]) ||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export const canCommissionMatterExternal = (hass: HomeAssistant) =>
|
||||
hass.auth.external?.config.canCommissionMatter;
|
||||
|
||||
export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
if (isComponentLoaded(hass, "thread")) {
|
||||
if (isComponentLoaded(hass.config, "thread")) {
|
||||
const datasets = await listThreadDataSets(hass);
|
||||
const preferredDataset = datasets.datasets.find(
|
||||
(dataset) => dataset.preferred
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import {
|
||||
filterNavigationPages,
|
||||
type NavigationFilterOptions,
|
||||
} from "../common/config/filter_navigation_pages";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
@@ -163,7 +163,7 @@ export const generateNavigationCommands = (
|
||||
filterOptions
|
||||
);
|
||||
const appItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass.config, "hassio")) {
|
||||
appItems.push({
|
||||
path: "/config/apps/available",
|
||||
icon_path: mdiStorePlus,
|
||||
|
||||
@@ -60,6 +60,7 @@ export type Selector =
|
||||
| NumberSelector
|
||||
| NumericThresholdSelector
|
||||
| ObjectSelector
|
||||
| PeriodSelector
|
||||
| AssistPipelineSelector
|
||||
| QRCodeSelector
|
||||
| SelectSelector
|
||||
@@ -392,6 +393,27 @@ export interface ObjectSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type PeriodKey =
|
||||
| "today"
|
||||
| "yesterday"
|
||||
| "tomorrow"
|
||||
| "this_week"
|
||||
| "last_week"
|
||||
| "next_week"
|
||||
| "this_month"
|
||||
| "last_month"
|
||||
| "next_month"
|
||||
| "this_year"
|
||||
| "last_year"
|
||||
| "next_7d"
|
||||
| "next_30d"
|
||||
| "none";
|
||||
export interface PeriodSelector {
|
||||
period: {
|
||||
options: readonly PeriodKey[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AssistPipelineSelector {
|
||||
assist_pipeline: {
|
||||
include_last_used?: boolean;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user