mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-23 03:27:03 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c22e1cfe68 |
@@ -1,18 +1,12 @@
|
||||
---
|
||||
name: ha-frontend-testing
|
||||
description: Home Assistant frontend testing and validation workflow. Use when adding or updating tests, running lint, TypeScript checks, Vitest, Playwright e2e suites, dev servers, or chart-data benchmarks.
|
||||
description: Home Assistant frontend validation workflow. Use when running lint, TypeScript checks, Vitest, Playwright e2e suites, dev servers, or chart-data benchmarks.
|
||||
---
|
||||
|
||||
# HA Frontend Testing
|
||||
|
||||
Use this skill when choosing or running validation for frontend changes.
|
||||
|
||||
## Test Helpers
|
||||
|
||||
- Before adding or changing tests, inspect the relevant suite's existing helpers and fixtures. Reuse them instead of duplicating setup, test data, navigation, interactions, waits, or assertions.
|
||||
- When the same test flow appears more than once, move it into the closest suite-local helper with a focused interface.
|
||||
- Keep one-off test behaviour in the test unless a helper makes the intent materially clearer. Do not hide the behaviour under test behind broad, configurable abstractions.
|
||||
|
||||
## Core Commands
|
||||
|
||||
```bash
|
||||
@@ -41,7 +35,7 @@ For focused type feedback on one file, use editor diagnostics instead of a file-
|
||||
|
||||
`yarn dev:serve` also serves locally and supports `-c` for the core URL and `-p` for the port. The default is 8124, or 8123 in a devcontainer.
|
||||
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`. Prefer managed background mode while iterating so the watcher stays available across test runs without occupying the terminal.
|
||||
Dev server commands support `--background`, `--status`, `--stop`, and `--logs [--follow]`.
|
||||
|
||||
## Playwright E2E
|
||||
|
||||
@@ -49,17 +43,15 @@ Each suite has its own dev server port. Playwright reuses an existing server loc
|
||||
|
||||
Start the relevant suite server, then run that suite:
|
||||
|
||||
| Suite | Background server | Test command |
|
||||
| ------- | -------------------------------------------- | ----------------------- |
|
||||
| App | `yarn test:e2e:app:dev --background` on 8095 | `yarn test:e2e:app` |
|
||||
| Demo | `yarn dev:demo --background` on 8090 | `yarn test:e2e:demo` |
|
||||
| Gallery | `yarn dev:gallery --background` on 8100 | `yarn test:e2e:gallery` |
|
||||
| Suite | Server | Test command |
|
||||
| ------- | ------------------------------- | ----------------------- |
|
||||
| App | `yarn test:e2e:app:dev` on 8095 | `yarn test:e2e:app` |
|
||||
| Demo | `yarn dev:demo` on 8090 | `yarn test:e2e:demo` |
|
||||
| Gallery | `yarn dev:gallery` on 8100 | `yarn test:e2e:gallery` |
|
||||
|
||||
The custom development wrappers use `/__ha_dev_status` to identify and manage their own suites. Playwright server reuse checks the configured URL instead. Wrapper start and stop operations are idempotent for a matching suite and reject an unrelated process occupying the port.
|
||||
|
||||
Local runs against a watched development server do not always match CI's clean build artifacts, environment, sharding, or worker configuration. Use background servers for the fast iteration loop, but confirm the relevant CI jobs complete successfully before considering E2E changes verified.
|
||||
|
||||
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites in parallel when every managed server is available, otherwise it runs them sequentially to prevent cold builds racing over shared generated assets. Run suites directly; piping through output truncation hides progress and failures.
|
||||
Use `-g "<title>" --project=chromium` to narrow a run. `yarn test:e2e` runs all three suites. Run suites directly; piping through output truncation hides progress and failures.
|
||||
|
||||
The app suite uses a stripped-down harness for e2e. Demo and gallery use their normal dev servers.
|
||||
|
||||
|
||||
@@ -8,37 +8,16 @@ inputs:
|
||||
cache:
|
||||
description: Enable the yarn cache in setup-node
|
||||
default: "true"
|
||||
node-modules-cache:
|
||||
description: Restore the exact shared node_modules cache before installing
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Restore complete dependency tree
|
||||
id: dependency-cache
|
||||
if: inputs.node-modules-cache == 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: ${{ inputs.cache == 'true' && (inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true') && 'yarn' || '' }}
|
||||
|
||||
- name: Enable Corepack
|
||||
shell: bash
|
||||
run: corepack enable
|
||||
cache: ${{ inputs.cache == 'true' && 'yarn' || '' }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: inputs.node-modules-cache != 'true' || steps.dependency-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: yarn install ${{ inputs.immutable == 'true' && '--immutable' || '' }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
@@ -22,56 +21,16 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare-dependencies:
|
||||
name: Prepare dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for complete dependency tree
|
||||
id: dependencies
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
lookup-only: true
|
||||
- name: Setup Node and install
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
cache: false
|
||||
- name: Save complete dependency tree
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
.yarn/install-state.gz
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'yarn.lock', '.yarnrc.yml', '.yarn/releases/**', '.yarn/patches/**') }}
|
||||
|
||||
lint:
|
||||
name: Lint and check format
|
||||
needs: prepare-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Check for duplicate dependencies
|
||||
run: yarn dedupe --check
|
||||
- name: Build resources
|
||||
@@ -104,17 +63,14 @@ jobs:
|
||||
run: yarn run lint:licenses
|
||||
test:
|
||||
name: Run tests
|
||||
needs: prepare-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
|
||||
env:
|
||||
@@ -124,7 +80,6 @@ jobs:
|
||||
build:
|
||||
name: Build frontend
|
||||
needs:
|
||||
- prepare-dependencies
|
||||
- lint
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -133,10 +88,8 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Build Application
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
|
||||
+40
-161
@@ -38,9 +38,8 @@ jobs:
|
||||
- name: Build demo
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-demo-e2e
|
||||
target: build-demo
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
is-test: true
|
||||
|
||||
- name: Upload demo build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
@@ -66,9 +65,8 @@ jobs:
|
||||
- name: Build e2e test app
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-e2e-test-app-e2e
|
||||
target: build-e2e-test-app
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
is-test: true
|
||||
|
||||
- name: Upload e2e test app build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
@@ -105,27 +103,15 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Run Playwright tests against Chromium ──────────────────────────────────
|
||||
e2e-demo:
|
||||
name: E2E demo (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-demo
|
||||
# ── Run Playwright tests locally against Chromium ──────────────────────────
|
||||
e2e-local:
|
||||
name: E2E (local Chromium)
|
||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex:
|
||||
- 1
|
||||
- 2
|
||||
shardTotal:
|
||||
- 2
|
||||
# Fail fast if anything hangs. The whole suite should take < 15 minutes on
|
||||
# Chromium; anything longer is almost certainly an install or webServer
|
||||
# hang.
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
@@ -135,133 +121,60 @@ jobs:
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
# Resolve the installed Playwright version so the browser cache tracks
|
||||
# Playwright itself, not every unrelated dependency bump.
|
||||
- name: Resolve Playwright version
|
||||
id: playwright-version
|
||||
run: echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Cache the downloaded browser build keyed on the installed Playwright
|
||||
# version, so re-runs skip the ~170 MB download unless Playwright changes.
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: yarn playwright install --with-deps chromium
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Run Playwright demo tests
|
||||
run: yarn test:e2e:demo --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload demo blob report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-demo-${{ matrix.shardIndex }}
|
||||
path: test/e2e/reports/demo/
|
||||
if-no-files-found: warn
|
||||
retention-days: 3
|
||||
|
||||
e2e-app:
|
||||
name: E2E app (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-e2e-test-app
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
shardTotal:
|
||||
- 4
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Run Playwright app tests
|
||||
run: yarn test:e2e:app --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload app blob report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-app-${{ matrix.shardIndex }}
|
||||
path: test/e2e/reports/app/
|
||||
if-no-files-found: warn
|
||||
retention-days: 3
|
||||
|
||||
e2e-gallery:
|
||||
name: E2E gallery (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
needs:
|
||||
- build-gallery
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
shardTotal:
|
||||
- 4
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
|
||||
- name: Run Playwright gallery tests
|
||||
run: yarn test:e2e:gallery --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
- name: Run Playwright tests (local)
|
||||
run: yarn test:e2e
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload gallery blob report
|
||||
- name: Upload blob report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-gallery-${{ matrix.shardIndex }}
|
||||
path: test/e2e/reports/gallery/
|
||||
if-no-files-found: warn
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
retention-days: 3
|
||||
|
||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
||||
report:
|
||||
name: Report
|
||||
needs:
|
||||
- e2e-demo
|
||||
- e2e-app
|
||||
- e2e-gallery
|
||||
needs: [e2e-local]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
if: ${{ !cancelled() }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -274,26 +187,12 @@ jobs:
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Download demo blob reports
|
||||
- name: Download blob report (local)
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-demo-*
|
||||
path: test/e2e/reports/demo/
|
||||
|
||||
- name: Download app blob reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-app-*
|
||||
path: test/e2e/reports/app/
|
||||
|
||||
- name: Download gallery blob reports
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
pattern: blob-report-gallery-*
|
||||
path: test/e2e/reports/gallery/
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
|
||||
- name: Stage blobs for merge
|
||||
run: node test/e2e/collect-blob-reports.mjs
|
||||
@@ -310,11 +209,7 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
- name: Post report to PR
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
(needs.e2e-demo.result == 'failure' ||
|
||||
needs.e2e-app.result == 'failure' ||
|
||||
needs.e2e-gallery.result == 'failure')
|
||||
if: github.event_name == 'pull_request' && needs.e2e-local.result == 'failure'
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
@@ -322,19 +217,3 @@ jobs:
|
||||
`${process.env.GITHUB_WORKSPACE}/test/e2e/post-report-comment.mjs`
|
||||
);
|
||||
await postReportComment({ github, context, core });
|
||||
|
||||
- name: Check suite results
|
||||
run: |
|
||||
failed=0
|
||||
for suite in \
|
||||
"demo:${{ needs.e2e-demo.result }}" \
|
||||
"app:${{ needs.e2e-app.result }}" \
|
||||
"gallery:${{ needs.e2e-gallery.result }}"; do
|
||||
name="${suite%%:*}"
|
||||
result="${suite#*:}"
|
||||
echo "E2E ${name}: ${result}"
|
||||
if [ "$result" != "success" ]; then
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
exit "$failed"
|
||||
|
||||
@@ -232,7 +232,7 @@ module.exports.config = {
|
||||
};
|
||||
},
|
||||
|
||||
demo({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
|
||||
demo({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||
return {
|
||||
name: "demo" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
@@ -247,7 +247,6 @@ module.exports.config = {
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -307,7 +306,7 @@ module.exports.config = {
|
||||
};
|
||||
},
|
||||
|
||||
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) {
|
||||
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||
return {
|
||||
name: "e2e-test-app" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
@@ -322,7 +321,6 @@ module.exports.config = {
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,22 +42,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-demo-e2e",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-demo",
|
||||
// Cast needs to be backwards compatible and older HA has no translations
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-demo",
|
||||
"rspack-prod-demo-e2e",
|
||||
"gen-pages-demo-prod-e2e"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"analyze-demo",
|
||||
gulp.series(
|
||||
|
||||
@@ -39,18 +39,3 @@ gulp.task(
|
||||
"gen-pages-e2e-test-app-prod"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-e2e-test-app-e2e",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-e2e-test-app",
|
||||
"rspack-prod-e2e-test-app-e2e",
|
||||
"gen-pages-e2e-test-app-prod"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -225,16 +225,6 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-demo-prod-e2e",
|
||||
genPagesProdTask(
|
||||
DEMO_PAGE_ENTRIES,
|
||||
paths.demo_dir,
|
||||
paths.demo_output_root,
|
||||
paths.demo_output_latest
|
||||
)
|
||||
);
|
||||
|
||||
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||
|
||||
gulp.task(
|
||||
|
||||
@@ -177,18 +177,6 @@ gulp.task("rspack-prod-demo", () =>
|
||||
bothBuilds(createDemoConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-prod-demo-e2e", () =>
|
||||
prodBuild(
|
||||
createDemoConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -281,18 +269,6 @@ gulp.task("rspack-prod-e2e-test-app", () =>
|
||||
bothBuilds(createE2eTestAppConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-prod-e2e-test-app-e2e", () =>
|
||||
prodBuild(
|
||||
createE2eTestAppConfig({
|
||||
isProdBuild: true,
|
||||
latestBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -387,14 +387,9 @@ const createAppConfig = ({
|
||||
bundle.config.app({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
|
||||
);
|
||||
|
||||
const createDemoConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}) =>
|
||||
const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
createRspackConfig(
|
||||
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild, isTestBuild })
|
||||
bundle.config.demo({ isProdBuild, latestBuild, isStatsBuild })
|
||||
);
|
||||
|
||||
const createCastConfig = ({ isProdBuild, latestBuild }) =>
|
||||
@@ -406,19 +401,9 @@ const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||
|
||||
const createE2eTestAppConfig = ({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
}) =>
|
||||
const createE2eTestAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
createRspackConfig(
|
||||
bundle.config.e2eTestApp({
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
isTestBuild,
|
||||
})
|
||||
bundle.config.e2eTestApp({ isProdBuild, latestBuild, isStatsBuild })
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@
|
||||
"@lit/task": "1.0.3",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/web": "2.5.0",
|
||||
"@material/web": "2.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
@@ -196,7 +196,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.1.0",
|
||||
"lint-staged": "17.0.8",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
|
||||
@@ -58,17 +58,6 @@
|
||||
"depNameTemplate": "rhysd/actionlint",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"description": "Keep Playwright CI container image up to date",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/workflows/e2e\\.yaml$/"],
|
||||
"matchStrings": [
|
||||
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)"
|
||||
],
|
||||
"depNameTemplate": "mcr.microsoft.com/playwright",
|
||||
"datasourceTemplate": "docker",
|
||||
"versioningTemplate": "regex:^v(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)-(?<compatibility>noble)$"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
@@ -102,11 +91,6 @@
|
||||
"description": "Group formatjs monorepo package",
|
||||
"groupName": "formatjs",
|
||||
"matchPackageNames": ["@formatjs/**"]
|
||||
},
|
||||
{
|
||||
"description": "Group Playwright package and CI container updates",
|
||||
"groupName": "Playwright",
|
||||
"matchPackageNames": ["@playwright/test", "mcr.microsoft.com/playwright"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,17 +6,11 @@ export const canToggleDomain = (hass: HomeAssistant, domain: string) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (domain === "button" || domain === "input_button") {
|
||||
return "press" in services;
|
||||
}
|
||||
if (domain === "lock") {
|
||||
return "lock" in services;
|
||||
}
|
||||
if (domain === "cover") {
|
||||
return "open_cover" in services;
|
||||
}
|
||||
if (domain === "valve") {
|
||||
return "open_valve" in services;
|
||||
}
|
||||
return "turn_on" in services;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { canToggleDomain } from "./can_toggle_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
import { ClimateEntityFeature } from "../../data/climate";
|
||||
|
||||
export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
@@ -27,10 +26,7 @@ export const canToggleState = (hass: HomeAssistant, stateObj: HassEntity) => {
|
||||
}
|
||||
|
||||
if (domain === "climate") {
|
||||
return (
|
||||
supportsFeature(stateObj, ClimateEntityFeature.TURN_ON) &&
|
||||
supportsFeature(stateObj, ClimateEntityFeature.TURN_OFF)
|
||||
);
|
||||
return supportsFeature(stateObj, 4096);
|
||||
}
|
||||
|
||||
return canToggleDomain(hass, domain);
|
||||
|
||||
@@ -89,7 +89,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
device_class: [
|
||||
"battery",
|
||||
"battery_charging",
|
||||
"carbon_monoxide",
|
||||
"co",
|
||||
"cold",
|
||||
"connectivity",
|
||||
"door",
|
||||
@@ -227,12 +227,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
"voltage",
|
||||
"volume_flow_rate",
|
||||
],
|
||||
state_class: [
|
||||
"measurement",
|
||||
"measurement_angle",
|
||||
"total",
|
||||
"total_increasing",
|
||||
],
|
||||
state_class: ["measurement", "total", "total_increasing"],
|
||||
},
|
||||
switch: {
|
||||
device_class: ["outlet", "switch"],
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
// A range smaller than this fraction of the axis magnitude is floating-point
|
||||
// noise (e.g. from summed statistics), not real precision.
|
||||
const NEGLIGIBLE_RANGE_RATIO = 1e-10;
|
||||
|
||||
// Derive the number of decimal digits to use for Y-axis labels from the
|
||||
// observed data range. We mirror how ECharts sizes its ticks: it splits the
|
||||
// range into ~5 intervals (its default `splitNumber`) and rounds that raw
|
||||
// interval to a "nice" 1/2/3/5×10ⁿ value, then reports the decimals that nice
|
||||
// interval needs. This matches the precision ECharts actually renders, so
|
||||
// labels are neither truncated to identical values nor padded with extra zeros.
|
||||
export function computeYAxisFractionDigits(
|
||||
min: number,
|
||||
max: number,
|
||||
// Bar axes render from 0, so union the extent with 0 to match.
|
||||
includeZero = false
|
||||
): number {
|
||||
const lo = includeZero ? Math.min(min, 0) : min;
|
||||
const hi = includeZero ? Math.max(max, 0) : max;
|
||||
const range = hi - lo;
|
||||
export function computeYAxisFractionDigits(min: number, max: number): number {
|
||||
const range = max - min;
|
||||
if (!Number.isFinite(range) || range <= 0) return 1;
|
||||
// A near-zero range is fp noise; deriving digits from it would pad the labels
|
||||
// with a tail of zeros (e.g. "0.20000000000000"), so treat it as flat.
|
||||
const magnitude = Math.max(Math.abs(lo), Math.abs(hi));
|
||||
if (range <= magnitude * NEGLIGIBLE_RANGE_RATIO) return 1;
|
||||
const rawInterval = range / 5;
|
||||
const exponent = Math.floor(Math.log10(rawInterval));
|
||||
const mantissa = rawInterval / 10 ** exponent; // in [1, 10)
|
||||
@@ -53,7 +38,9 @@ const resolveYAxisBound = (
|
||||
export function createYAxisPrecisionBounds(options: {
|
||||
min?: YAxisBound;
|
||||
max?: YAxisBound;
|
||||
// Set for bar axes anchored at 0, so precision reflects the 0-based range.
|
||||
// Axes without `scale: true` (e.g. bar charts) stay anchored at 0, so the
|
||||
// rendered ticks span from 0 even when the data does not. Union the extent
|
||||
// with 0 to match the labels ECharts actually draws.
|
||||
includeZero?: boolean;
|
||||
onFractionDigits: (digits: number) => void;
|
||||
}): {
|
||||
@@ -65,11 +52,13 @@ export function createYAxisPrecisionBounds(options: {
|
||||
min: (values) => {
|
||||
const resolvedMin = resolveYAxisBound(min, values);
|
||||
const resolvedMax = resolveYAxisBound(max, values);
|
||||
const extentMin = resolvedMin ?? values.min;
|
||||
const extentMax = resolvedMax ?? values.max;
|
||||
onFractionDigits(
|
||||
computeYAxisFractionDigits(extentMin, extentMax, includeZero)
|
||||
);
|
||||
let extentMin = resolvedMin ?? values.min;
|
||||
let extentMax = resolvedMax ?? values.max;
|
||||
if (includeZero) {
|
||||
extentMin = Math.min(extentMin, 0);
|
||||
extentMax = Math.max(extentMax, 0);
|
||||
}
|
||||
onFractionDigits(computeYAxisFractionDigits(extentMin, extentMax));
|
||||
return resolvedMin;
|
||||
},
|
||||
max: (values) => resolveYAxisBound(max, values),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AssistChip } from "@material/web/chips/internal/assist-chip";
|
||||
import { styles } from "@material/web/chips/internal/assist-styles.cssresult.js";
|
||||
import { styles } from "@material/web/chips/internal/assist-styles";
|
||||
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
|
||||
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles.cssresult.js";
|
||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
|
||||
import { FilterChip } from "@material/web/chips/internal/filter-chip";
|
||||
import { styles } from "@material/web/chips/internal/filter-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import { styles } from "@material/web/chips/internal/filter-styles";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { InputChip } from "@material/web/chips/internal/input-chip";
|
||||
import { styles } from "@material/web/chips/internal/input-styles.cssresult.js";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles.cssresult.js";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles.cssresult.js";
|
||||
import { styles } from "@material/web/chips/internal/input-styles";
|
||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
|
||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
|
||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const HIDDEN_ATTRIBUTES = [
|
||||
"battery_level",
|
||||
"code_arm_required",
|
||||
"code_format",
|
||||
"color_modes",
|
||||
"device_class",
|
||||
"editable",
|
||||
"effect_list",
|
||||
|
||||
@@ -6,10 +6,8 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../common/color/compute-color";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateActive } from "../../common/entity/state_active";
|
||||
import {
|
||||
stateColorBrightness,
|
||||
stateColorCss,
|
||||
@@ -29,15 +27,10 @@ export class StateBadge extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public overrideImage?: string;
|
||||
|
||||
/**
|
||||
* Cannot be a boolean attribute because undefined is treated different than
|
||||
* false. When it is undefined, state is still colored for light entities.
|
||||
* @deprecated use `color` instead
|
||||
*/
|
||||
// Cannot be a boolean attribute because undefined is treated different than
|
||||
// false. When it is undefined, state is still colored for light entities.
|
||||
@property({ attribute: false }) public stateColor?: boolean;
|
||||
|
||||
// "state", "none" or a color (theme color name or CSS color). Custom colors
|
||||
// apply when the entity is active. Takes precedence over stateColor.
|
||||
@property() public color?: string;
|
||||
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@@ -74,17 +67,11 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _color(): string | undefined {
|
||||
if (this.color) {
|
||||
return this.color;
|
||||
}
|
||||
if (this.stateColor !== undefined) {
|
||||
return this.stateColor ? "state" : "none";
|
||||
}
|
||||
private get _stateColor() {
|
||||
const domain = this.stateObj
|
||||
? computeStateDomain(this.stateObj)
|
||||
: undefined;
|
||||
return domain === "light" ? "state" : undefined;
|
||||
return this.stateColor ?? domain === "light";
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -144,7 +131,6 @@ export class StateBadge extends LitElement {
|
||||
|
||||
if (stateObj) {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const color = this._color;
|
||||
if (this.overrideImage === undefined) {
|
||||
// hide icon if we have entity picture
|
||||
if (
|
||||
@@ -161,10 +147,13 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
this.icon = false;
|
||||
} else if (color === "state") {
|
||||
const stateColor = stateColorCss(stateObj);
|
||||
if (stateColor) {
|
||||
iconStyle.color = stateColor;
|
||||
} else if (this.color) {
|
||||
// Externally provided overriding color wins over state color
|
||||
iconStyle.color = this.color;
|
||||
} else if (this._stateColor) {
|
||||
const color = stateColorCss(stateObj);
|
||||
if (color) {
|
||||
iconStyle.color = color;
|
||||
}
|
||||
if (stateObj.attributes.rgb_color) {
|
||||
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
||||
@@ -191,8 +180,6 @@ export class StateBadge extends LitElement {
|
||||
delete iconStyle.color;
|
||||
}
|
||||
}
|
||||
} else if (color && color !== "none" && stateActive(stateObj)) {
|
||||
iconStyle.color = computeCssColor(color);
|
||||
}
|
||||
} else if (this.overrideImage) {
|
||||
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "../ha-tooltip";
|
||||
@@ -24,11 +23,10 @@ class StateInfo extends LitElement {
|
||||
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
// Inline style because the state-badge color API only colors active entities
|
||||
return html`<state-badge
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${!this.color}
|
||||
style=${styleMap({ color: this.color })}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
></state-badge>
|
||||
<div class="info">
|
||||
<div class="name ${this.inDialog ? "in-dialog" : ""}" .title=${name}>
|
||||
|
||||
@@ -57,16 +57,6 @@ interface AssistMessage {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const initialPromptToSubmit = (
|
||||
prompt: string | undefined,
|
||||
submit: boolean
|
||||
): string | undefined => (submit ? prompt?.trim() || undefined : undefined);
|
||||
|
||||
export const assistPipelineChanged = (
|
||||
previous: AssistPipeline | undefined,
|
||||
current: AssistPipeline | undefined
|
||||
): boolean => previous?.id !== current?.id;
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
@@ -77,12 +67,6 @@ export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public initialPrompt?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public submitInitialPrompt = false;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaInput;
|
||||
|
||||
@query(".message:last-child")
|
||||
@@ -115,8 +99,6 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _initialPromptSubmitted = false;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
|
||||
private _audioBuffer?: Int16Array[];
|
||||
@@ -126,11 +108,7 @@ export class HaAssistChat extends LitElement {
|
||||
private _stt_binary_handler_id?: number | null;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
(changedProperties.has("pipeline") &&
|
||||
assistPipelineChanged(changedProperties.get("pipeline"), this.pipeline))
|
||||
) {
|
||||
if (!this.hasUpdated || changedProperties.has("pipeline")) {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
@@ -160,20 +138,6 @@ export class HaAssistChat extends LitElement {
|
||||
if (changedProps.has("_conversation")) {
|
||||
this._scrollMessagesBottom();
|
||||
}
|
||||
if (
|
||||
!this._initialPromptSubmitted &&
|
||||
(changedProps.has("initialPrompt") ||
|
||||
changedProps.has("submitInitialPrompt"))
|
||||
) {
|
||||
const prompt = initialPromptToSubmit(
|
||||
this.initialPrompt,
|
||||
this.submitInitialPrompt
|
||||
);
|
||||
if (prompt) {
|
||||
this._initialPromptSubmitted = true;
|
||||
this._processText(prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
|
||||
@@ -57,40 +57,17 @@ export const evaluateCondition = (
|
||||
return matchFieldCondition(condition, data);
|
||||
};
|
||||
|
||||
export const isFieldVisible = (
|
||||
export const isFieldHidden = (
|
||||
schema: HaFormSchema,
|
||||
data: HaFormDataContainer | undefined
|
||||
): boolean => {
|
||||
const { visible } = schema as HaFormBaseSchema;
|
||||
if (visible === undefined || visible === true) {
|
||||
return true;
|
||||
}
|
||||
if (visible === false) {
|
||||
const { hidden } = schema as HaFormBaseSchema;
|
||||
if (!hidden) {
|
||||
return false;
|
||||
}
|
||||
const conditions = Array.isArray(visible) ? visible : [visible];
|
||||
if (hidden === true) {
|
||||
return true;
|
||||
}
|
||||
const conditions = Array.isArray(hidden) ? hidden : [hidden];
|
||||
return conditions.every((condition) => evaluateCondition(condition, data));
|
||||
};
|
||||
|
||||
// Hiding a field drops its value, which can flip another field's condition, so
|
||||
// resolve the set to a fixpoint.
|
||||
export const getHiddenFields = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data: HaFormDataContainer | undefined
|
||||
): Set<string> => {
|
||||
const hidden = new Set<string>();
|
||||
const evalData: HaFormDataContainer = { ...(data ?? {}) };
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const field of schema) {
|
||||
if (hidden.has(field.name) || isFieldVisible(field, evalData)) {
|
||||
continue;
|
||||
}
|
||||
hidden.add(field.name);
|
||||
delete evalData[field.name];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getHiddenFields } from "./conditions";
|
||||
import { isFieldHidden } from "./conditions";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
@@ -68,11 +68,9 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hiddenFields = getHiddenFields(this.schema.schema, this.data);
|
||||
|
||||
return html`
|
||||
${this.schema.schema
|
||||
.filter((item) => !hiddenFields.has(item.name))
|
||||
.filter((item) => !isFieldHidden(item, this.data))
|
||||
.map(
|
||||
(item) => html`
|
||||
<ha-form
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-selector/ha-selector";
|
||||
import { getHiddenFields } from "./conditions";
|
||||
import { isFieldHidden } from "./conditions";
|
||||
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
|
||||
|
||||
const LOAD_ELEMENTS = {
|
||||
@@ -99,9 +99,8 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
const hiddenFields = getHiddenFields(this.schema, this.data);
|
||||
const visibleSchema = this.schema.filter(
|
||||
(item) => !hiddenFields.has(item.name)
|
||||
(item) => !isFieldHidden(item, this.data)
|
||||
);
|
||||
|
||||
visibleSchema.forEach((item, index) => {
|
||||
@@ -158,8 +157,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const renderHiddenFields = getHiddenFields(this.schema, this.data);
|
||||
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
${
|
||||
@@ -172,7 +169,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: ""
|
||||
}
|
||||
${this.schema.map((item) => {
|
||||
if (renderHiddenFields.has(item.name)) {
|
||||
if (isFieldHidden(item, this.data)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ export interface HaFormBaseSchema {
|
||||
default?: HaFormData;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
// Field is visible while the condition holds (visible by default).
|
||||
// Serializable so it can be shared with the backend and other renderers.
|
||||
visible?: boolean | HaFormCondition | HaFormCondition[];
|
||||
// Field is hidden while the condition holds. Serializable so it can be
|
||||
// shared with the backend and other renderers.
|
||||
hidden?: boolean | HaFormCondition | HaFormCondition[];
|
||||
description?: {
|
||||
suffix?: string;
|
||||
// This value will be set initially when form is loaded
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ListItemEl } from "@material/web/list/internal/listitem/list-item";
|
||||
import { styles } from "@material/web/list/internal/listitem/list-item-styles.cssresult.js";
|
||||
import { styles } from "@material/web/list/internal/listitem/list-item-styles";
|
||||
import { css, html, nothing, type TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "./ha-ripple";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { List } from "@material/web/list/internal/list";
|
||||
import { styles } from "@material/web/list/internal/list-styles.cssresult.js";
|
||||
import { styles } from "@material/web/list/internal/list-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OutlinedButton } from "@material/web/button/internal/outlined-button";
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles.cssresult.js";
|
||||
import { styles } from "@material/web/button/internal/outlined-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/button/internal/shared-styles";
|
||||
import { styles } from "@material/web/button/internal/outlined-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OutlinedField } from "@material/web/field/internal/outlined-field";
|
||||
import { styles } from "@material/web/field/internal/outlined-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles.cssresult.js";
|
||||
import { styles } from "@material/web/field/internal/outlined-styles";
|
||||
import { styles as sharedStyles } from "@material/web/field/internal/shared-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconButton } from "@material/web/iconbutton/internal/icon-button";
|
||||
import { styles } from "@material/web/iconbutton/internal/outlined-styles.cssresult.js";
|
||||
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles.cssresult.js";
|
||||
import { styles } from "@material/web/iconbutton/internal/outlined-styles";
|
||||
import { styles as sharedStyles } from "@material/web/iconbutton/internal/shared-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
|
||||
import { Ripple } from "@material/web/ripple/internal/ripple";
|
||||
import { styles } from "@material/web/ripple/internal/ripple-styles.cssresult.js";
|
||||
import { styles } from "@material/web/ripple/internal/ripple-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { initialState } from "@lit/task";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { AsyncValueTask } from "../../common/controllers/async-value-task";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../../data/context";
|
||||
import { entityIcon } from "../../data/icons";
|
||||
import type { IconSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-picker";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("ha-selector-icon")
|
||||
export class HaIconSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: IconSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -34,21 +30,10 @@ export class HaIconSelector extends LitElement {
|
||||
icon_entity?: string;
|
||||
};
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["context", "icon_entity"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
private get _stateObj(): HassEntity | undefined {
|
||||
const iconEntity = this.context?.icon_entity;
|
||||
return iconEntity ? this.hass.states[iconEntity] : undefined;
|
||||
}
|
||||
|
||||
private _placeholderTask = new AsyncValueTask(this, {
|
||||
task: ([
|
||||
@@ -59,31 +44,19 @@ export class HaIconSelector extends LitElement {
|
||||
connection,
|
||||
stateObj,
|
||||
]) => {
|
||||
if (
|
||||
placeholder ||
|
||||
attributeIcon ||
|
||||
!entities ||
|
||||
!config ||
|
||||
!connection ||
|
||||
!stateObj
|
||||
) {
|
||||
if (placeholder || attributeIcon || !stateObj) {
|
||||
return initialState;
|
||||
}
|
||||
return entityIcon(
|
||||
entities,
|
||||
config.config,
|
||||
connection.connection,
|
||||
stateObj
|
||||
);
|
||||
return entityIcon(entities, config, connection, stateObj);
|
||||
},
|
||||
args: () => {
|
||||
const stateObj = this._stateObj;
|
||||
return [
|
||||
this.selector.icon?.placeholder,
|
||||
stateObj?.attributes.icon,
|
||||
this._entities,
|
||||
this._config,
|
||||
this._connection,
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj,
|
||||
] as const;
|
||||
},
|
||||
@@ -99,6 +72,7 @@ export class HaIconSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SubMenu } from "@material/web/menu/internal/submenu/sub-menu";
|
||||
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles.cssresult.js";
|
||||
import { styles } from "@material/web/menu/internal/submenu/sub-menu-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ export const NON_NUMERIC_ATTRIBUTES = [
|
||||
"away_mode",
|
||||
"changed_by",
|
||||
"code_format",
|
||||
"color_modes",
|
||||
"current_activity",
|
||||
"device_class",
|
||||
"editable",
|
||||
@@ -176,7 +177,6 @@ export const NON_NUMERIC_ATTRIBUTES = [
|
||||
"source_type",
|
||||
"source",
|
||||
"state_class",
|
||||
"supported_color_modes",
|
||||
"supported_features",
|
||||
"swing_mode",
|
||||
"swing_mode",
|
||||
@@ -190,6 +190,7 @@ export const NON_NUMERIC_ATTRIBUTES = [
|
||||
export const STATE_CONDITION_HIDDEN_ATTRIBUTES = [
|
||||
"access_token",
|
||||
"available_modes",
|
||||
"color_modes",
|
||||
"editable",
|
||||
"effect_list",
|
||||
"entity_picture",
|
||||
@@ -206,7 +207,6 @@ export const STATE_CONDITION_HIDDEN_ATTRIBUTES = [
|
||||
"sound_mode_list",
|
||||
"source_list",
|
||||
"state_class",
|
||||
"supported_color_modes",
|
||||
"swing_modes",
|
||||
"token",
|
||||
];
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface SurveyInteraction {
|
||||
date: string;
|
||||
action: "opened" | "dismissed";
|
||||
}
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showEntityIdPicker?: boolean;
|
||||
default_panel?: string;
|
||||
@@ -21,9 +16,6 @@ export interface CoreFrontendSystemData {
|
||||
default_panel?: string;
|
||||
onboarded_version?: string;
|
||||
onboarded_date?: string;
|
||||
surveys?: {
|
||||
onboarding?: SurveyInteraction;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HomeFrontendSystemData {
|
||||
|
||||
@@ -149,7 +149,6 @@ export const weatherAttrIcons = {
|
||||
humidity: mdiWaterPercent,
|
||||
wind_bearing: mdiWeatherWindy,
|
||||
wind_speed: mdiWeatherWindy,
|
||||
wind_gust_speed: mdiWeatherWindy,
|
||||
pressure: mdiGauge,
|
||||
temperature: mdiThermometer,
|
||||
uv_index: mdiSunWireless,
|
||||
@@ -269,7 +268,6 @@ export const getWeatherUnit = (
|
||||
return (
|
||||
stateObj.attributes.temperature_unit || config.unit_system.temperature
|
||||
);
|
||||
case "wind_gust_speed":
|
||||
case "wind_speed":
|
||||
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
|
||||
case "cloud_coverage":
|
||||
|
||||
@@ -9,7 +9,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||
import "../../components/ha-alert";
|
||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||
import { getHiddenFields } from "../../components/ha-form/conditions";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -227,17 +226,14 @@ class StepFlowForm extends LitElement {
|
||||
const checkAllRequiredFields = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data: Record<string, any>
|
||||
) => {
|
||||
const hidden = getHiddenFields(schema, data);
|
||||
return schema.every(
|
||||
) =>
|
||||
schema.every(
|
||||
(field) =>
|
||||
hidden.has(field.name) ||
|
||||
((!field.required || !["", undefined].includes(data[field.name])) &&
|
||||
(field.type !== "expandable" ||
|
||||
(!field.required && data[field.name] === undefined) ||
|
||||
checkAllRequiredFields(field.schema, data[field.name])))
|
||||
(!field.required || !["", undefined].includes(data[field.name])) &&
|
||||
(field.type !== "expandable" ||
|
||||
(!field.required && data[field.name] === undefined) ||
|
||||
checkAllRequiredFields(field.schema, data[field.name]))
|
||||
);
|
||||
};
|
||||
|
||||
const allRequiredInfoFilledIn =
|
||||
stepData === undefined
|
||||
@@ -259,14 +255,8 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
const flowId = this.step.flow_id;
|
||||
|
||||
const hiddenFields = getHiddenFields(this.step.data_schema, stepData);
|
||||
|
||||
const toSendData: Record<string, unknown> = {};
|
||||
Object.keys(stepData).forEach((key) => {
|
||||
if (hiddenFields.has(key)) {
|
||||
// Hidden fields are not part of the submitted config
|
||||
return;
|
||||
}
|
||||
const value = stepData[key];
|
||||
const isEmpty = [undefined, ""].includes(value);
|
||||
const field = this.step.data_schema?.find((f) => f.name === key);
|
||||
|
||||
@@ -78,18 +78,21 @@ class MoreInfoLawnMower extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private get _startPauseIcon(): string {
|
||||
if (!this.stateObj) return mdiPlay;
|
||||
return isMowing(this.stateObj) &&
|
||||
private get _showPause(): boolean {
|
||||
if (!this.stateObj) return false;
|
||||
return (
|
||||
isMowing(this.stateObj) &&
|
||||
supportsFeature(this.stateObj, LawnMowerEntityFeature.PAUSE)
|
||||
? mdiPause
|
||||
: mdiPlay;
|
||||
);
|
||||
}
|
||||
|
||||
private get _startPauseIcon(): string {
|
||||
return this._showPause ? mdiPause : mdiPlay;
|
||||
}
|
||||
|
||||
private get _startPauseLabel(): string {
|
||||
if (!this.stateObj) return "";
|
||||
return isMowing(this.stateObj) &&
|
||||
supportsFeature(this.stateObj, LawnMowerEntityFeature.PAUSE)
|
||||
return this._showPause
|
||||
? this._i18n.localize("ui.dialogs.more_info_control.lawn_mower.pause")
|
||||
: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.lawn_mower.start_mowing"
|
||||
@@ -99,8 +102,11 @@ class MoreInfoLawnMower extends LitElement {
|
||||
private get _startPauseDisabled(): boolean {
|
||||
if (!this.stateObj) return true;
|
||||
if (this.stateObj.state === UNAVAILABLE) return true;
|
||||
if (isMowing(this.stateObj)) return false;
|
||||
return !canStartMowing(this.stateObj);
|
||||
if (this._showPause) return false;
|
||||
return (
|
||||
!supportsFeature(this.stateObj, LawnMowerEntityFeature.START_MOWING) ||
|
||||
!canStartMowing(this.stateObj)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderBattery() {
|
||||
@@ -156,7 +162,7 @@ class MoreInfoLawnMower extends LitElement {
|
||||
private _handleStartPause() {
|
||||
if (!this.stateObj) return;
|
||||
forwardHaptic(this, "light");
|
||||
if (isMowing(this.stateObj)) {
|
||||
if (this._showPause) {
|
||||
this._api.callService("lawn_mower", "pause", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiCommentProcessingOutline, mdiDevices } from "@mdi/js";
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -61,7 +61,6 @@ import { isIosApp } from "../../util/is_ios";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import {
|
||||
effectiveQuickBarMode,
|
||||
type QuickBarParams,
|
||||
@@ -70,18 +69,6 @@ import {
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
interface AssistComboBoxItem extends PickerComboBoxItem {
|
||||
action: "assist";
|
||||
assistPrompt: string;
|
||||
}
|
||||
|
||||
type QuickBarComboBoxItem =
|
||||
| NavigationComboBoxItem
|
||||
| ActionCommandComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
| AssistComboBoxItem;
|
||||
|
||||
@customElement("ha-quick-bar")
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -303,7 +290,13 @@ export class QuickBar extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow = (item: QuickBarComboBoxItem) => {
|
||||
private _renderRow = (
|
||||
item:
|
||||
| NavigationComboBoxItem
|
||||
| ActionCommandComboBoxItem
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -419,8 +412,8 @@ export class QuickBar extends LitElement {
|
||||
}: {
|
||||
firstIndex: number;
|
||||
lastIndex: number;
|
||||
firstItem: QuickBarComboBoxItem | string;
|
||||
secondItem: QuickBarComboBoxItem | string;
|
||||
firstItem: PickerComboBoxItem | string;
|
||||
secondItem: PickerComboBoxItem | string;
|
||||
itemsCount: number;
|
||||
}) => {
|
||||
if (
|
||||
@@ -468,23 +461,7 @@ export class QuickBar extends LitElement {
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | QuickBarComboBoxItem)[] = [];
|
||||
const prompt = filter?.trim();
|
||||
const assistItem =
|
||||
prompt &&
|
||||
(!section || section === "command") &&
|
||||
isComponentLoaded(this.hass.config, "conversation") &&
|
||||
!this.hass.auth.external?.config.hasAssist
|
||||
? ({
|
||||
id: "ask-assist",
|
||||
action: "assist",
|
||||
primary: this.hass.localize("ui.dialogs.quick-bar.ask_assist", {
|
||||
query: prompt,
|
||||
}),
|
||||
icon_path: mdiCommentProcessingOutline,
|
||||
assistPrompt: prompt,
|
||||
} satisfies AssistComboBoxItem)
|
||||
: undefined;
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -509,12 +486,10 @@ export class QuickBar extends LitElement {
|
||||
items.push(...navigateItems);
|
||||
}
|
||||
|
||||
if (!section || section === "command") {
|
||||
let commandItems = this.hass.user?.is_admin
|
||||
? this._generateActionCommandsMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
)
|
||||
: [];
|
||||
if (this.hass.user?.is_admin && (!section || section === "command")) {
|
||||
let commandItems = this._generateActionCommandsMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
commandItems = this._filterGroup(
|
||||
@@ -524,15 +499,12 @@ export class QuickBar extends LitElement {
|
||||
) as ActionCommandComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!section && (commandItems.length || assistItem)) {
|
||||
if (!section && commandItems.length) {
|
||||
// show group title
|
||||
items.push(this.hass.localize("ui.dialogs.quick-bar.commands_title"));
|
||||
}
|
||||
|
||||
items.push(...commandItems);
|
||||
if (assistItem) {
|
||||
items.push(assistItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
@@ -751,21 +723,10 @@ export class QuickBar extends LitElement {
|
||||
const { index, newTab } = ev.detail;
|
||||
const item = this._comboBox.virtualizerElement.items[
|
||||
index
|
||||
] as QuickBarComboBoxItem;
|
||||
] as PickerComboBoxItem;
|
||||
|
||||
this._itemSelected = true;
|
||||
|
||||
if (item && "assistPrompt" in item) {
|
||||
this.closeDialog();
|
||||
showVoiceCommandDialog(this, this.hass, {
|
||||
pipeline_id: "last_used",
|
||||
start_listening: false,
|
||||
prompt: item.assistPrompt,
|
||||
submit: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// entity selected
|
||||
if (item && "stateObj" in item) {
|
||||
this.closeDialog();
|
||||
|
||||
@@ -58,11 +58,9 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
private _startListening = false;
|
||||
|
||||
private _prompt?: string;
|
||||
|
||||
private _submitPrompt = false;
|
||||
|
||||
public async showDialog(params: VoiceCommandDialogParams): Promise<void> {
|
||||
public async showDialog(
|
||||
params: Required<VoiceCommandDialogParams>
|
||||
): Promise<void> {
|
||||
await this._loadPipelines();
|
||||
const pipelinesIds = this._pipelines?.map((pipeline) => pipeline.id) || [];
|
||||
if (
|
||||
@@ -79,9 +77,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
this._pipelineId = this._preferredPipeline;
|
||||
}
|
||||
|
||||
this._startListening = params.start_listening ?? false;
|
||||
this._prompt = params.prompt;
|
||||
this._submitPrompt = params.submit ?? false;
|
||||
this._startListening = params.start_listening;
|
||||
this._dialogOpen = true;
|
||||
this._open = true;
|
||||
}
|
||||
@@ -193,8 +189,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.pipeline=${this._pipeline}
|
||||
.startListening=${this._startListening}
|
||||
.initialPrompt=${this._prompt}
|
||||
.submitInitialPrompt=${this._submitPrompt}
|
||||
>
|
||||
</ha-assist-chat>
|
||||
`
|
||||
|
||||
@@ -6,8 +6,6 @@ const loadVoiceCommandDialog = () => import("./ha-voice-command-dialog");
|
||||
export interface VoiceCommandDialogParams {
|
||||
pipeline_id: "last_used" | "preferred" | string;
|
||||
start_listening?: boolean;
|
||||
prompt?: string;
|
||||
submit?: boolean;
|
||||
}
|
||||
|
||||
export const showVoiceCommandDialog = (
|
||||
@@ -33,8 +31,6 @@ export const showVoiceCommandDialog = (
|
||||
pipeline_id: dialogParams.pipeline_id,
|
||||
// Don't start listening by default for web
|
||||
start_listening: dialogParams.start_listening ?? false,
|
||||
prompt: dialogParams.prompt,
|
||||
submit: dialogParams.submit ?? false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ interface EMOutgoingMessageConnectionStatus extends EMMessage {
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageFrontendLoaded extends EMMessage {
|
||||
type: "frontend/loaded"; // Fired once the launch screen is removed; with hasSplashscreen this is after the first panel has rendered
|
||||
type: "frontend/loaded"; // Fired once the launch screen is removed (connected and essential data loaded)
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageAppConfiguration extends EMMessage {
|
||||
@@ -371,7 +371,6 @@ export interface ExternalConfig {
|
||||
appVersion?: string;
|
||||
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
|
||||
hasAssistSettings?: boolean; // Shows the "This device" section in voice assistant settings
|
||||
hasSplashscreen?: boolean; // App covers the frontend with its own loading screen until frontend/loaded, so the launch screen is removed without animation
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToAction {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
removeLaunchScreen,
|
||||
renderLaunchScreenInfoBox,
|
||||
} from "../util/launch-screen";
|
||||
import { checkOnboardingSurveyToast } from "../util/onboarding-survey";
|
||||
import {
|
||||
registerServiceWorker,
|
||||
supportsServiceWorker,
|
||||
@@ -60,8 +59,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _httpPendingDialogOpen = false;
|
||||
|
||||
private _onboardingSurveyChecked = false;
|
||||
|
||||
private _panelUrl: string;
|
||||
|
||||
@storage({ key: "ha-version", state: false, subscribe: false })
|
||||
@@ -111,17 +108,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
) {
|
||||
this.checkHttpPendingConfig();
|
||||
}
|
||||
if (
|
||||
changedProps.has("hass") &&
|
||||
!this._onboardingSurveyChecked &&
|
||||
this.hass?.user &&
|
||||
this.hass.systemData
|
||||
) {
|
||||
this._onboardingSurveyChecked = true;
|
||||
if (!__DEMO__) {
|
||||
checkOnboardingSurveyToast(this, this.hass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
@@ -133,12 +119,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
// Apps with a native splash screen keep covering the frontend until
|
||||
// frontend/loaded, so the launch screen stays up (invisibly) until the
|
||||
// first panel has rendered and partial-panel-resolver removes it.
|
||||
if (!this.hass.auth.external?.config.hasSplashscreen) {
|
||||
removeLaunchScreen();
|
||||
}
|
||||
removeLaunchScreen();
|
||||
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
|
||||
}
|
||||
super.update(changedProps);
|
||||
|
||||
@@ -220,8 +220,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
) {
|
||||
await this.rebuild();
|
||||
await this.pageRendered;
|
||||
removeLaunchScreen(!!this.hass.auth.external?.config.hasSplashscreen);
|
||||
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
|
||||
removeLaunchScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface ShowToastParams {
|
||||
message:
|
||||
string | { translationKey: LocalizeKeys; args?: Record<string, string> };
|
||||
action?: ToastActionParams;
|
||||
dismiss?: () => void;
|
||||
duration?: number;
|
||||
dismissable?: boolean;
|
||||
bottomOffset?: number;
|
||||
@@ -72,10 +71,7 @@ class NotificationManager extends LitElement {
|
||||
this._toast?.show();
|
||||
}
|
||||
|
||||
private _toastClosed(ev: HASSDomEvent<ToastClosedEventDetail>) {
|
||||
if (ev.detail.reason === "dismiss") {
|
||||
this._parameters?.dismiss?.();
|
||||
}
|
||||
private _toastClosed(_ev: HASSDomEvent<ToastClosedEventDetail>) {
|
||||
this._parameters = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ import { showMoreInfoDialog } from "../../../../../dialogs/more-info/show-ha-mor
|
||||
import { MobileAwareMixin } from "../../../../../mixins/mobile-aware-mixin";
|
||||
import { mdiHomeAssistant } from "../../../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistantRegistries, Route } from "../../../../../types";
|
||||
import type { Route } from "../../../../../types";
|
||||
import { bytesToString } from "../../../../../util/bytes-to-string";
|
||||
import { getAppDisplayName } from "../../common/app";
|
||||
import "../../components/supervisor-apps-state";
|
||||
@@ -172,20 +172,10 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._computeUpdateEntityId();
|
||||
this._startPolling();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Registries load asynchronously and change over time; resolve the update
|
||||
// entity reactively instead of only once on connect.
|
||||
this._updateEntityId = this._findUpdateEntityId(
|
||||
this.registries.devices,
|
||||
this.registries.entities,
|
||||
(this._currentAddon as HassioAddonDetails).slug
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopPolling();
|
||||
@@ -1510,24 +1500,26 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
"system_managed" in addon && addon.system_managed
|
||||
);
|
||||
|
||||
private _findUpdateEntityId = memoizeOne(
|
||||
(
|
||||
devices: HomeAssistantRegistries["devices"],
|
||||
entities: HomeAssistantRegistries["entities"],
|
||||
slug: string
|
||||
): string | undefined => {
|
||||
const device = Object.values(devices).find((d) =>
|
||||
d.identifiers.some(([domain, id]) => domain === "hassio" && id === slug)
|
||||
);
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.values(entities).find(
|
||||
(e) =>
|
||||
e.device_id === device.id && computeDomain(e.entity_id) === "update"
|
||||
)?.entity_id;
|
||||
private _computeUpdateEntityId() {
|
||||
const addon = this._currentAddon as HassioAddonDetails;
|
||||
const device = Object.values(this.registries.devices).find((d) =>
|
||||
d.identifiers.some(
|
||||
([domain, id]) => domain === "hassio" && id === addon.slug
|
||||
)
|
||||
);
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
const updateEntity = Object.values(this.registries.entities).find(
|
||||
(e) =>
|
||||
e.device_id === device.id && computeDomain(e.entity_id) === "update"
|
||||
);
|
||||
if (!updateEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateEntityId = updateEntity.entity_id;
|
||||
}
|
||||
|
||||
private _openUpdate() {
|
||||
showMoreInfoDialog(this, { entityId: this._updateEntityId! });
|
||||
|
||||
@@ -30,6 +30,7 @@ const SCHEMA = [
|
||||
"code_arm_required",
|
||||
"code_format",
|
||||
"color_mode",
|
||||
"color_modes",
|
||||
"current_activity",
|
||||
"device_class",
|
||||
"editable",
|
||||
|
||||
@@ -80,6 +80,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
"available_modes",
|
||||
"code_arm_required",
|
||||
"code_format",
|
||||
"color_modes",
|
||||
"device_class",
|
||||
"editable",
|
||||
"effect_list",
|
||||
|
||||
@@ -16,7 +16,11 @@ import "../../../components/ha-svg-icon";
|
||||
import { apiContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { LawnMowerEntity } from "../../../data/lawn_mower";
|
||||
import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower";
|
||||
import {
|
||||
LawnMowerEntityFeature,
|
||||
canDock,
|
||||
canStartMowing,
|
||||
} from "../../../data/lawn_mower";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
@@ -72,6 +76,9 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record<
|
||||
translationKey: "start",
|
||||
icon: mdiPlay,
|
||||
serviceName: "start_mowing",
|
||||
disabled:
|
||||
!supportsFeature(stateObj, LawnMowerEntityFeature.START_MOWING) ||
|
||||
!canStartMowing(stateObj),
|
||||
};
|
||||
},
|
||||
dock: (stateObj) => ({
|
||||
|
||||
@@ -36,30 +36,6 @@ const MEASUREMENT_VARIANTS: Variant[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const MEASUREMENT_ANGLE_VARIANTS: Variant[] = [
|
||||
{
|
||||
labelKey: "last_24h",
|
||||
days_to_show: 1,
|
||||
period: "hour",
|
||||
chart_type: "line",
|
||||
stat_types: ["mean"],
|
||||
},
|
||||
{
|
||||
labelKey: "last_7d",
|
||||
days_to_show: 7,
|
||||
period: "day",
|
||||
chart_type: "line",
|
||||
stat_types: ["mean"],
|
||||
},
|
||||
{
|
||||
labelKey: "last_30d",
|
||||
days_to_show: 30,
|
||||
period: "day",
|
||||
chart_type: "line",
|
||||
stat_types: ["mean"],
|
||||
},
|
||||
];
|
||||
|
||||
const TOTAL_VARIANTS: Variant[] = [
|
||||
{
|
||||
labelKey: "last_7d",
|
||||
@@ -84,13 +60,6 @@ const TOTAL_VARIANTS: Variant[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const VARIANTS_BY_STATE_CLASS: Record<string, Variant[]> = {
|
||||
measurement: MEASUREMENT_VARIANTS,
|
||||
measurement_angle: MEASUREMENT_ANGLE_VARIANTS,
|
||||
total: TOTAL_VARIANTS,
|
||||
total_increasing: TOTAL_VARIANTS,
|
||||
};
|
||||
|
||||
export const statisticsGraphCardSuggestions: CardSuggestionProvider<StatisticsGraphCardConfig> =
|
||||
{
|
||||
getEntitySuggestion(hass, entityId) {
|
||||
@@ -98,8 +67,8 @@ export const statisticsGraphCardSuggestions: CardSuggestionProvider<StatisticsGr
|
||||
const stateObj = hass.states[entityId];
|
||||
const stateClass = stateObj?.attributes.state_class;
|
||||
if (!stateClass) return null;
|
||||
const variants = VARIANTS_BY_STATE_CLASS[stateClass];
|
||||
if (!variants) return null;
|
||||
const variants =
|
||||
stateClass === "measurement" ? MEASUREMENT_VARIANTS : TOTAL_VARIANTS;
|
||||
const suggestions: CardSuggestion<StatisticsGraphCardConfig>[] =
|
||||
variants.map((v) => ({
|
||||
label: hass.localize(`${LABEL_PREFIX}${v.labelKey}` as any),
|
||||
|
||||
@@ -521,7 +521,7 @@ export function generateEnergyDevicesDetailGraphData(
|
||||
true,
|
||||
generateFillBuckets(datasets, start, end, getSuggestedPeriod(start, end))
|
||||
);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
|
||||
|
||||
return {
|
||||
chartData: datasets,
|
||||
|
||||
@@ -119,7 +119,7 @@ export function generateEnergyGasGraphData(
|
||||
true,
|
||||
generateFillBuckets(datasets, start, end, period)
|
||||
);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
|
||||
const chartData = datasets;
|
||||
const total = processTotal(energyData.stats, gasSources);
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export function generateEnergySolarGraphData(
|
||||
end,
|
||||
compareStart,
|
||||
compareEnd,
|
||||
yAxisFractionDigits: computeYAxisFractionDigits(yMin, yMax, true),
|
||||
yAxisFractionDigits: computeYAxisFractionDigits(yMin, yMax),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ const stackOrder = {
|
||||
to_grid: 2,
|
||||
used_solar: 3,
|
||||
used_battery: 4,
|
||||
from_grid: 5,
|
||||
used_grid: 5,
|
||||
};
|
||||
|
||||
@customElement("hui-energy-usage-graph-card")
|
||||
@@ -297,15 +295,6 @@ export class HuiEnergyUsageGraphCard
|
||||
to_battery: {},
|
||||
};
|
||||
|
||||
// Grid sources can be import-only or export-only; assign color indices by
|
||||
// position in the user's grid sources config so this card matches the
|
||||
// energy sources table, which uses positional indices.
|
||||
const colorIndices: Record<string, Record<string, number>> = {};
|
||||
Object.keys(colorPropertyMap).forEach((key) => {
|
||||
colorIndices[key] = {};
|
||||
});
|
||||
let gridIdx = 0;
|
||||
|
||||
for (const source of energyData.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (statIds.solar) {
|
||||
@@ -344,7 +333,6 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.from_grid = [gridSource.stat_energy_from];
|
||||
}
|
||||
colorIndices.from_grid[gridSource.stat_energy_from] = gridIdx;
|
||||
if (gridSource.name) {
|
||||
statLabels.from_grid[gridSource.stat_energy_from] =
|
||||
gridSource.stat_energy_to
|
||||
@@ -361,7 +349,6 @@ export class HuiEnergyUsageGraphCard
|
||||
} else {
|
||||
statIds.to_grid = [gridSource.stat_energy_to];
|
||||
}
|
||||
colorIndices.to_grid[gridSource.stat_energy_to] = gridIdx;
|
||||
if (gridSource.name) {
|
||||
statLabels.to_grid[gridSource.stat_energy_to] =
|
||||
gridSource.stat_energy_from
|
||||
@@ -372,18 +359,17 @@ export class HuiEnergyUsageGraphCard
|
||||
: gridSource.name;
|
||||
}
|
||||
}
|
||||
gridIdx++;
|
||||
}
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
const colorIndices: Record<string, Record<string, number>> = {};
|
||||
Object.keys(colorPropertyMap).forEach((key) => {
|
||||
colorIndices[key] = {};
|
||||
if (
|
||||
key === "used_grid" ||
|
||||
key === "used_solar" ||
|
||||
key === "used_battery" ||
|
||||
key === "from_grid" ||
|
||||
key === "to_grid"
|
||||
key === "used_battery"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -475,7 +461,7 @@ export class HuiEnergyUsageGraphCard
|
||||
getSuggestedPeriod(this._start, this._end)
|
||||
)
|
||||
);
|
||||
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
|
||||
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
|
||||
this._chartData = datasets;
|
||||
this._legendData = this._getLegendData(datasets);
|
||||
this._total = this._processTotal(consumption);
|
||||
|
||||
@@ -274,7 +274,7 @@ export class HuiEnergyWaterGraphCard
|
||||
getSuggestedPeriod(this._start, this._end)
|
||||
)
|
||||
);
|
||||
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
|
||||
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
|
||||
this._chartData = datasets;
|
||||
this._total = this._processTotal(energyData.stats, waterSources);
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export function generatePowerSourcesGraphData(
|
||||
const end = energyData.end || endOfToday();
|
||||
|
||||
const chartData = fillLineGaps(datasets);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax, true);
|
||||
const yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
|
||||
|
||||
const usageData: NonNullable<LineSeriesOption["data"]> = [];
|
||||
// fillLineGaps ensures all datasets share the same x values, so iterate the
|
||||
|
||||
@@ -8,7 +8,6 @@ import "../../../components/ha-card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { applyDefaultColor } from "../common/entity-color-config";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import "../components/hui-entities-toggle";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
@@ -25,7 +24,7 @@ import type {
|
||||
LovelaceHeaderFooter,
|
||||
} from "../types";
|
||||
import { migrateEntitiesCardConfig } from "./migrate-card-config";
|
||||
import type { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types";
|
||||
import type { EntitiesCardConfig } from "./types";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
|
||||
export const computeShowHeaderToggle = <
|
||||
@@ -160,7 +159,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
const migratedConfig = migrateEntitiesCardConfig(config);
|
||||
const entities = processConfigEntities(migratedConfig.entities);
|
||||
|
||||
this._config = { color: "state", ...migratedConfig };
|
||||
this._config = migratedConfig;
|
||||
this._configEntities = entities;
|
||||
this._showHeaderToggle = computeShowHeaderToggle(migratedConfig, entities);
|
||||
if (this._config.header) {
|
||||
@@ -344,18 +343,18 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
`,
|
||||
];
|
||||
|
||||
private _rowConfig(entityConf: LovelaceRowConfig): LovelaceRowConfig {
|
||||
if (entityConf.type === "perform-action") {
|
||||
return { ...entityConf, type: "call-service" };
|
||||
}
|
||||
return applyDefaultColor(
|
||||
entityConf as EntitiesCardEntityConfig,
|
||||
this._config!.color
|
||||
);
|
||||
}
|
||||
|
||||
private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
|
||||
const element = createRowElement(this._rowConfig(entityConf));
|
||||
const element = createRowElement(
|
||||
(!("type" in entityConf) || entityConf.type === "conditional") &&
|
||||
"state_color" in this._config!
|
||||
? ({
|
||||
state_color: this._config.state_color,
|
||||
...(entityConf as EntityConfig),
|
||||
} as EntityConfig)
|
||||
: entityConf.type === "perform-action"
|
||||
? { ...entityConf, type: "call-service" }
|
||||
: entityConf
|
||||
);
|
||||
if (this._hass) {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction, hasAnyAction } from "../common/has-action";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { applyDefaultColor } from "../common/entity-color-config";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
@@ -88,19 +87,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
show_name: true,
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
color: "state",
|
||||
state_color: true,
|
||||
...migratedConfig,
|
||||
};
|
||||
const cardColor = this._config.color;
|
||||
const entities = processConfigEntities(migratedConfig.entities).map(
|
||||
(entityConf) =>
|
||||
applyDefaultColor(
|
||||
{
|
||||
hold_action: { action: "more-info" } as MoreInfoActionConfig,
|
||||
...entityConf,
|
||||
},
|
||||
cardColor
|
||||
)
|
||||
(entityConf) => ({
|
||||
hold_action: { action: "more-info" } as MoreInfoActionConfig,
|
||||
...entityConf,
|
||||
})
|
||||
);
|
||||
|
||||
for (const entity of entities) {
|
||||
@@ -300,7 +294,9 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
.color=${entityConf.color}
|
||||
.stateColor=${
|
||||
entityConf.state_color ?? this._config!.state_color
|
||||
}
|
||||
></state-badge>
|
||||
`
|
||||
: ""
|
||||
|
||||
@@ -1,56 +1,40 @@
|
||||
import { migrateStateColorConfig } from "../common/entity-color-config";
|
||||
import { migrateTimeFormatConfig } from "../common/entity-time-format-config";
|
||||
import type {
|
||||
ConditionalRowConfig,
|
||||
LovelaceRowConfig,
|
||||
} from "../entity-rows/types";
|
||||
import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
EntitiesCardEntityConfig,
|
||||
GlanceCardConfig,
|
||||
GlanceConfigEntity,
|
||||
} from "./types";
|
||||
|
||||
const migrateEntitiesRowConfig = (
|
||||
rowConf: LovelaceRowConfig | string
|
||||
): LovelaceRowConfig | string => {
|
||||
if (typeof rowConf !== "object") {
|
||||
return rowConf;
|
||||
}
|
||||
let newConf: LovelaceRowConfig = rowConf;
|
||||
newConf = migrateTimeFormatConfig(newConf as EntitiesCardEntityConfig);
|
||||
newConf = migrateStateColorConfig(newConf as EntitiesCardEntityConfig);
|
||||
if (newConf.type === "conditional") {
|
||||
const row = (newConf as ConditionalRowConfig).row;
|
||||
if (row && typeof row === "object") {
|
||||
let newRow = migrateTimeFormatConfig(row as EntitiesCardEntityConfig);
|
||||
newRow = migrateStateColorConfig(newRow);
|
||||
if (newRow !== row) {
|
||||
newConf = { ...newConf, row: newRow } as ConditionalRowConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newConf;
|
||||
};
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
const newConf = migrateEntitiesRowConfig(e);
|
||||
if (newConf !== e) {
|
||||
changed = true;
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
return newConf;
|
||||
// Custom rows own their config schema and may use `format` with a
|
||||
// different meaning (e.g. custom:multiple-entity-row), so leave it
|
||||
// untouched.
|
||||
if (e.type?.startsWith("custom:")) {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
const newConfig = migrateStateColorConfig(config);
|
||||
if (!changed) {
|
||||
return newConfig;
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...newConfig,
|
||||
entities: newEntities,
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -62,20 +46,21 @@ export const migrateGlanceCardConfig = (
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
let newConf = e;
|
||||
newConf = migrateTimeFormatConfig(newConf);
|
||||
newConf = migrateStateColorConfig(newConf);
|
||||
if (newConf !== e) {
|
||||
changed = true;
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
return newConf;
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
const newConfig = migrateStateColorConfig(config);
|
||||
if (!changed) {
|
||||
return newConfig;
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...newConfig,
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,9 +112,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
/** @deprecated use `color` instead */
|
||||
state_color?: boolean;
|
||||
color?: string;
|
||||
show_name?: boolean;
|
||||
show_icon?: boolean;
|
||||
}
|
||||
@@ -128,9 +126,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
|
||||
icon?: string;
|
||||
header?: LovelaceHeaderFooterConfig;
|
||||
footer?: LovelaceHeaderFooterConfig;
|
||||
/** @deprecated use `color` instead */
|
||||
state_color?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export type AreaCardDisplayType = "compact" | "icon" | "picture" | "camera";
|
||||
@@ -334,9 +330,7 @@ export interface GlanceConfigEntity extends ConfigEntity {
|
||||
show_last_changed?: boolean;
|
||||
image?: string;
|
||||
show_state?: boolean;
|
||||
/** @deprecated use `color` instead */
|
||||
state_color?: boolean;
|
||||
color?: string;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
@@ -348,9 +342,7 @@ export interface GlanceCardConfig extends LovelaceCardConfig {
|
||||
theme?: string;
|
||||
entities: (string | GlanceConfigEntity)[];
|
||||
columns?: number;
|
||||
/** @deprecated use `color` instead */
|
||||
state_color?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface HumidifierCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { isCustomType } from "../../../data/lovelace_custom_cards";
|
||||
|
||||
interface LegacyStateColorConfig {
|
||||
type?: string;
|
||||
color?: string;
|
||||
state_color?: boolean;
|
||||
}
|
||||
|
||||
export const migrateStateColorConfig = <T extends LegacyStateColorConfig>(
|
||||
config: T
|
||||
): T => {
|
||||
// Custom elements own their config schema, leave them untouched
|
||||
if (config.type !== undefined && isCustomType(config.type)) {
|
||||
return config;
|
||||
}
|
||||
if (config.state_color === undefined) {
|
||||
return config;
|
||||
}
|
||||
const { state_color, ...rest } = config;
|
||||
return {
|
||||
color: state_color ? "state" : "none",
|
||||
...rest,
|
||||
} as T;
|
||||
};
|
||||
|
||||
export const applyDefaultColor = <T extends { type?: string; color?: string }>(
|
||||
config: T,
|
||||
color: string | undefined
|
||||
): T =>
|
||||
color === undefined ||
|
||||
config.color !== undefined ||
|
||||
(config.type !== undefined && isCustomType(config.type))
|
||||
? config
|
||||
: { ...config, color };
|
||||
@@ -1,27 +0,0 @@
|
||||
import { isCustomType } from "../../../data/lovelace_custom_cards";
|
||||
import type { TimestampRenderingFormat } from "../components/types";
|
||||
|
||||
interface LegacyTimeFormatConfig {
|
||||
type?: string;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
/** @deprecated use `time_format` instead */
|
||||
format?: TimestampRenderingFormat;
|
||||
}
|
||||
|
||||
export const migrateTimeFormatConfig = <T extends LegacyTimeFormatConfig>(
|
||||
config: T
|
||||
): T => {
|
||||
// Custom elements own their config schema and may use the same option with
|
||||
// a different meaning (e.g. custom:multiple-entity-row), leave them untouched
|
||||
if (config.type !== undefined && isCustomType(config.type)) {
|
||||
return config;
|
||||
}
|
||||
if (config.format === undefined) {
|
||||
return config;
|
||||
}
|
||||
const { format, ...rest } = config;
|
||||
return {
|
||||
time_format: format,
|
||||
...rest,
|
||||
} as T;
|
||||
};
|
||||
@@ -22,9 +22,6 @@ import type {
|
||||
} from "../../../data/lovelace/config/action";
|
||||
import type { ServiceAction } from "../../../data/script";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { canToggleState } from "../../../common/entity/can_toggle_state";
|
||||
import { canToggleDomain } from "../../../common/entity/can_toggle_domain";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
|
||||
|
||||
@@ -137,35 +134,15 @@ export class HuiActionEditor extends LitElement {
|
||||
]
|
||||
);
|
||||
|
||||
private _filterToggleAction = memoizeOne(
|
||||
(actions: UiAction[]): UiAction[] => {
|
||||
return actions.filter((a) => a !== "toggle");
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const actions = this.actions ?? DEFAULT_ACTIONS;
|
||||
|
||||
let action = this.config?.action || (this.required ? "" : "default");
|
||||
|
||||
let actions = this.actions ?? DEFAULT_ACTIONS;
|
||||
|
||||
if (
|
||||
this.context?.entity_id &&
|
||||
action !== "toggle" &&
|
||||
actions.includes("toggle")
|
||||
) {
|
||||
const stateObj = this.hass.states[this.context.entity_id];
|
||||
const canToggle = stateObj
|
||||
? canToggleState(this.hass, stateObj)
|
||||
: canToggleDomain(this.hass, computeDomain(this.context.entity_id));
|
||||
if (!canToggle) {
|
||||
actions = this._filterToggleAction(actions);
|
||||
}
|
||||
}
|
||||
|
||||
if (action === "call-service") {
|
||||
action = "perform-action";
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ export class HuiGenericEntityRow extends LitElement {
|
||||
.overrideIcon=${this.config.icon}
|
||||
.overrideImage=${this.config.image}
|
||||
.stateColor=${this.config.state_color}
|
||||
.color=${this.config.color}
|
||||
></state-badge>
|
||||
${
|
||||
!this.hideName
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
|
||||
interface CustomizableListSchemaParams {
|
||||
field: string;
|
||||
customize: boolean;
|
||||
options: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export const customizableListSchema = ({
|
||||
field,
|
||||
customize,
|
||||
options,
|
||||
}: CustomizableListSchemaParams) =>
|
||||
[
|
||||
@@ -14,18 +16,21 @@ export const customizableListSchema = ({
|
||||
name: "customize",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: field,
|
||||
visible: { field: "customize", value: true },
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(customize
|
||||
? ([
|
||||
{
|
||||
name: field,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
// `customize` is form-only and never stored in the config.
|
||||
|
||||
@@ -37,7 +37,11 @@ export class HuiAlarmModesCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj: HassEntity | undefined) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity | undefined,
|
||||
customizeModes: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "customize_modes",
|
||||
@@ -45,24 +49,27 @@ export class HuiAlarmModesCardFeatureEditor
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "modes",
|
||||
visible: { field: "customize_modes", value: true },
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options: stateObj
|
||||
? supportedAlarmModes(stateObj).map((mode) => ({
|
||||
value: mode,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.alarm-modes.modes_list.${mode}`
|
||||
)}`,
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
...(customizeModes
|
||||
? ([
|
||||
{
|
||||
name: "modes",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options: stateObj
|
||||
? supportedAlarmModes(stateObj).map((mode) => ({
|
||||
value: mode,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.alarm-modes.modes_list.${mode}`
|
||||
)}`,
|
||||
}))
|
||||
: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
@@ -80,7 +87,11 @@ export class HuiAlarmModesCardFeatureEditor
|
||||
? this.hass.states[this.context?.entity_id]
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
data.customize_modes
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -113,8 +113,9 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: {
|
||||
hidden: {
|
||||
field: "clock_style",
|
||||
operator: "not_eq",
|
||||
value: "digital",
|
||||
},
|
||||
selector: {
|
||||
@@ -131,7 +132,7 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{
|
||||
name: "border",
|
||||
visible: { field: "clock_style", value: "analog" },
|
||||
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.border.description`
|
||||
@@ -144,7 +145,7 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{
|
||||
name: "ticks",
|
||||
visible: { field: "clock_style", value: "analog" },
|
||||
hidden: { field: "clock_style", operator: "not_eq", value: "analog" },
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.ticks.description`
|
||||
@@ -168,10 +169,13 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{
|
||||
name: "seconds_motion",
|
||||
visible: [
|
||||
{ field: "clock_style", value: "analog" },
|
||||
{ field: "show_seconds", value: true },
|
||||
],
|
||||
hidden: {
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "clock_style", operator: "not_eq", value: "analog" },
|
||||
{ field: "show_seconds", operator: "not_eq", value: true },
|
||||
],
|
||||
},
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
|
||||
@@ -195,10 +199,13 @@ export class HuiClockCardEditor
|
||||
},
|
||||
{
|
||||
name: "face_style",
|
||||
visible: [
|
||||
{ field: "clock_style", value: "analog" },
|
||||
{ field: "ticks", operator: "not_eq", value: "none" },
|
||||
],
|
||||
hidden: {
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "clock_style", operator: "not_eq", value: "analog" },
|
||||
{ field: "ticks", value: "none" },
|
||||
],
|
||||
},
|
||||
description: {
|
||||
suffix: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.face_style.description`
|
||||
|
||||
+4
-4
@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -33,12 +32,13 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((localize: LocalizeFunc) =>
|
||||
private _schema = memoizeOne((customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "actions",
|
||||
customize,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: localize(
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions_list.${action}`
|
||||
),
|
||||
})),
|
||||
@@ -51,7 +51,7 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
}
|
||||
|
||||
const data = customizableListData(this._config, "actions");
|
||||
const schema = this._schema(this.hass.localize);
|
||||
const schema = this._schema(data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -61,36 +61,38 @@ export class HuiEnergyDevicesCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((localize: LocalizeFunc) => {
|
||||
private _schema = memoizeOne((localize: LocalizeFunc, type: string) => {
|
||||
const schema: HaFormSchema[] = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "modes",
|
||||
required: false,
|
||||
visible: { field: "type", value: "energy-devices-graph" },
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: chartModeOpts.map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.energy-devices-graph.mode_options.${mode}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_compound_stats",
|
||||
required: false,
|
||||
visible: { field: "type", value: "energy-devices-graph" },
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(type === "energy-devices-graph"
|
||||
? ([
|
||||
{
|
||||
name: "modes",
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: chartModeOpts.map((mode) => ({
|
||||
value: mode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.energy-devices-graph.mode_options.${mode}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_compound_stats",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "max_devices",
|
||||
required: false,
|
||||
@@ -112,7 +114,7 @@ export class HuiEnergyDevicesCardEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
const schema = this._schema(this.hass.localize, this._config.type);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
@@ -23,36 +24,6 @@ import type {
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
{
|
||||
name: "title",
|
||||
visible: { field: "type", operator: "not_eq", value: "energy-compare" },
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "show_legend",
|
||||
visible: {
|
||||
field: "type",
|
||||
operator: "in",
|
||||
value: ["power-sources-graph", "energy-usage-graph"],
|
||||
},
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "link_dashboard",
|
||||
visible: { field: "type", value: "energy-distribution" },
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
name: "collection_key",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
@@ -101,11 +72,46 @@ export class HuiEnergyGraphCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((type: string) => {
|
||||
const schema: HaFormSchema[] = [
|
||||
...(type !== "energy-compare"
|
||||
? [{ name: "title", selector: { text: {} } }]
|
||||
: []),
|
||||
...(type === "power-sources-graph" || type === "energy-usage-graph"
|
||||
? [
|
||||
{
|
||||
name: "show_legend",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(type === "energy-distribution"
|
||||
? [
|
||||
{
|
||||
name: "link_dashboard",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
type: "string",
|
||||
name: "collection_key",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
return schema;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this._config.type);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
};
|
||||
@@ -113,7 +119,7 @@ export class HuiEnergyGraphCardEditor
|
||||
return html` <ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -22,9 +22,11 @@ import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { customType } from "../../../../common/structs/is-custom-type";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { isCustomType } from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
@@ -88,7 +90,6 @@ const conditionalEntitiesRowConfigStruct = object({
|
||||
type: literal("conditional"),
|
||||
row: any(),
|
||||
conditions: array(any()),
|
||||
color: optional(string()),
|
||||
});
|
||||
|
||||
const dividerEntitiesRowConfigStruct = object({
|
||||
@@ -180,22 +181,6 @@ const entitiesRowConfigStruct = dynamic<any>((value) => {
|
||||
return entitiesConfigStruct;
|
||||
});
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "show_header_toggle", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
@@ -204,7 +189,7 @@ const cardConfigStruct = assign(
|
||||
theme: optional(string()),
|
||||
icon: optional(string()),
|
||||
show_header_toggle: optional(boolean()),
|
||||
color: optional(string()),
|
||||
state_color: optional(boolean()),
|
||||
entities: array(entitiesRowConfigStruct),
|
||||
header: optional(headerFooterConfigStructs),
|
||||
footer: optional(headerFooterConfigStructs),
|
||||
@@ -241,6 +226,14 @@ export class HuiEntitiesCardEditor
|
||||
);
|
||||
});
|
||||
|
||||
get _title(): string {
|
||||
return this._config!.title || "";
|
||||
}
|
||||
|
||||
get _theme(): string {
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@@ -258,20 +251,52 @@ export class HuiEntitiesCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
show_header_toggle: this._showHeaderToggle(this._config),
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="card-config">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._formChanged}
|
||||
></ha-form>
|
||||
<ha-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.title"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._title}
|
||||
.configValue=${"title"}
|
||||
@input=${this._valueChanged}
|
||||
></ha-input>
|
||||
<ha-theme-picker
|
||||
.value=${this._theme}
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-theme-picker>
|
||||
<div class="side-by-side">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.entities.show_header_toggle"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._showHeaderToggle(this._config)}
|
||||
.configValue=${"show_header_toggle"}
|
||||
@change=${this._valueChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.state_color"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._config!.state_color}
|
||||
.configValue=${"state_color"}
|
||||
@change=${this._valueChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<hui-header-footer-editor
|
||||
.hass=${this.hass}
|
||||
.configValue=${"header"}
|
||||
@@ -296,43 +321,6 @@ export class HuiEntitiesCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _formChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { ...ev.detail.value } as EntitiesCardConfig;
|
||||
if (
|
||||
this._config.show_header_toggle === undefined &&
|
||||
config.show_header_toggle === this._showHeaderToggle(this._config)
|
||||
) {
|
||||
delete config.show_header_toggle;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "title":
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "show_header_toggle":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.entities.show_header_toggle"
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
@@ -342,7 +330,17 @@ export class HuiEntitiesCardEditor
|
||||
const target = ev.target! as EditorTarget;
|
||||
const configValue =
|
||||
target.configValue || this._subElementEditorConfig?.type;
|
||||
const value = ev.detail.config ?? ev.detail.value;
|
||||
const value =
|
||||
target.checked !== undefined
|
||||
? target.checked
|
||||
: target.value || ev.detail.config || ev.detail.value;
|
||||
|
||||
if (
|
||||
(configValue === "title" && target.value === this._title) ||
|
||||
(configValue === "theme" && target.value === this._theme)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configValue === "row" || (ev.detail && ev.detail.entities)) {
|
||||
const newConfigEntities =
|
||||
@@ -361,7 +359,7 @@ export class HuiEntitiesCardEditor
|
||||
this._config = { ...this._config!, entities: newConfigEntities };
|
||||
this._configEntities = processEditorEntities(this._config!.entities);
|
||||
} else if (configValue) {
|
||||
if (value === "" || value === undefined) {
|
||||
if (value === "") {
|
||||
this._config = { ...this._config };
|
||||
delete this._config[configValue!];
|
||||
} else {
|
||||
@@ -437,6 +435,10 @@ export class HuiEntitiesCardEditor
|
||||
hui-header-footer-editor {
|
||||
padding-top: var(--ha-space-1);
|
||||
}
|
||||
|
||||
ha-input {
|
||||
--ha-input-padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+29
-16
@@ -74,7 +74,11 @@ export abstract class HuiEntityModesCardFeatureEditorBase<
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj: HassEntity | undefined) => {
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity | undefined,
|
||||
customizeModes: boolean
|
||||
) => {
|
||||
const d = this.descriptor;
|
||||
const styleListId = d.styleListI18nFeatureId ?? d.i18nFeatureId;
|
||||
return [
|
||||
@@ -99,20 +103,25 @@ export abstract class HuiEntityModesCardFeatureEditorBase<
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: d.modeField,
|
||||
visible: { field: "customize_modes", value: true },
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options: d.getAvailableModesOrdered(stateObj).map((mode) => ({
|
||||
value: mode,
|
||||
label: d.getModeLabel(this.hass!, stateObj, mode),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(customizeModes
|
||||
? ([
|
||||
{
|
||||
name: d.modeField,
|
||||
selector: {
|
||||
select: {
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options: d
|
||||
.getAvailableModesOrdered(stateObj)
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
label: d.getModeLabel(this.hass!, stateObj, mode),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
}
|
||||
);
|
||||
@@ -134,7 +143,11 @@ export abstract class HuiEntityModesCardFeatureEditorBase<
|
||||
customize_modes: this._config[modeField as keyof TConfig] !== undefined,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
data.customize_modes
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -80,7 +80,7 @@ export class HuiGaugeCardEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(entityId?: string) =>
|
||||
(showSeverity: boolean, entityId?: string) =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
@@ -132,25 +132,28 @@ export class HuiGaugeCardEditor
|
||||
{ name: "show_severity", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "severity",
|
||||
type: "grid",
|
||||
visible: { field: "show_severity", value: true },
|
||||
schema: [
|
||||
{
|
||||
name: "green",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "yellow",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
...(showSeverity
|
||||
? ([
|
||||
{
|
||||
name: "severity",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "green",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "yellow",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
@@ -194,7 +197,10 @@ export class HuiGaugeCardEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this._config!.entity);
|
||||
const schema = this._schema(
|
||||
this._config!.severity !== undefined,
|
||||
this._config!.entity
|
||||
);
|
||||
const data = {
|
||||
show_severity: this._config!.severity !== undefined,
|
||||
...this._config,
|
||||
|
||||
@@ -59,28 +59,13 @@ export class HuiGenericEntityRowEditor
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: {
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secondary_info",
|
||||
|
||||
@@ -44,7 +44,7 @@ const cardConfigStruct = assign(
|
||||
show_name: optional(boolean()),
|
||||
show_state: optional(boolean()),
|
||||
show_icon: optional(boolean()),
|
||||
color: optional(string()),
|
||||
state_color: optional(boolean()),
|
||||
entities: array(entitiesConfigStruct),
|
||||
})
|
||||
);
|
||||
@@ -69,16 +69,7 @@ const SCHEMA = [
|
||||
{ name: "show_state", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "state_color", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-glance-card-editor")
|
||||
@@ -124,15 +115,6 @@ export class HuiGlanceCardEditor
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "show_last_changed", selector: { boolean: {} } },
|
||||
{ name: "show_state", selector: { boolean: {} }, default: true },
|
||||
],
|
||||
|
||||
@@ -102,11 +102,11 @@ const SCHEMA = [
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
visible: {
|
||||
condition: "or",
|
||||
hidden: {
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "min_y_axis", operator: "exists" },
|
||||
{ field: "max_y_axis", operator: "exists" },
|
||||
{ field: "min_y_axis", operator: "not_exists" },
|
||||
{ field: "max_y_axis", operator: "not_exists" },
|
||||
],
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
|
||||
+15
-13
@@ -34,18 +34,20 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((stateObj: HassEntity | undefined) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -58,7 +60,7 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj);
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -51,7 +51,7 @@ export class HuiMarkdownCardEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, text_only: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "style",
|
||||
@@ -73,11 +73,9 @@ export class HuiMarkdownCardEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
visible: { field: "style", operator: "not_eq", value: "text-only" },
|
||||
selector: { text: {} },
|
||||
},
|
||||
...(!text_only
|
||||
? ([{ name: "title", selector: { text: {} } }] as const)
|
||||
: []),
|
||||
{
|
||||
name: "content",
|
||||
required: true,
|
||||
@@ -133,7 +131,10 @@ export class HuiMarkdownCardEditor
|
||||
style: this._config.text_only ? "text-only" : "card",
|
||||
};
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this._config.text_only || false
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
+3
-2
@@ -38,10 +38,11 @@ export class HuiMediaPlayerPlaybackCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: MediaPlayerEntity | undefined) =>
|
||||
(stateObj: MediaPlayerEntity | undefined, customize: boolean) =>
|
||||
[
|
||||
...customizableListSchema({
|
||||
field: "controls",
|
||||
customize,
|
||||
options: MEDIA_PLAYER_PLAYBACK_CONTROLS.filter(
|
||||
(control) =>
|
||||
stateObj && supportsMediaPlayerPlaybackControl(stateObj, control)
|
||||
@@ -68,7 +69,7 @@ export class HuiMediaPlayerPlaybackCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
const data = customizableListData(this._config, "controls");
|
||||
const schema = this._schema(stateObj);
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
+16
-14
@@ -32,19 +32,21 @@ export class HuiMediaPlayerSoundModeCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((stateObj: MediaPlayerEntity | undefined) =>
|
||||
customizableListSchema({
|
||||
field: "sound_modes",
|
||||
options:
|
||||
stateObj?.attributes.sound_mode_list?.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"sound_mode",
|
||||
mode
|
||||
),
|
||||
})) ?? [],
|
||||
})
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: MediaPlayerEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "sound_modes",
|
||||
customize,
|
||||
options:
|
||||
stateObj?.attributes.sound_mode_list?.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"sound_mode",
|
||||
mode
|
||||
),
|
||||
})) ?? [],
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -58,7 +60,7 @@ export class HuiMediaPlayerSoundModeCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
const data = customizableListData(this._config, "sound_modes");
|
||||
const schema = this._schema(stateObj);
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
+16
-14
@@ -32,19 +32,21 @@ export class HuiMediaPlayerSourceCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((stateObj: MediaPlayerEntity | undefined) =>
|
||||
customizableListSchema({
|
||||
field: "sources",
|
||||
options:
|
||||
stateObj?.attributes.source_list?.map((source) => ({
|
||||
value: source,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"source",
|
||||
source
|
||||
),
|
||||
})) ?? [],
|
||||
})
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: MediaPlayerEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "sources",
|
||||
customize,
|
||||
options:
|
||||
stateObj?.attributes.source_list?.map((source) => ({
|
||||
value: source,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"source",
|
||||
source
|
||||
),
|
||||
})) ?? [],
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -58,7 +60,7 @@ export class HuiMediaPlayerSourceCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
const data = customizableListData(this._config, "sources");
|
||||
const schema = this._schema(stateObj);
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
+18
-18
@@ -38,10 +38,15 @@ export class HuiPrecipitationForecastCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: HassEntity | undefined, localize: HomeAssistant["localize"]) => {
|
||||
(
|
||||
stateObj: HassEntity | undefined,
|
||||
forecastType: ForecastResolution,
|
||||
localize: HomeAssistant["localize"]
|
||||
) => {
|
||||
const supportedTypes = stateObj
|
||||
? getSupportedForecastTypes(stateObj)
|
||||
: [];
|
||||
const isHourly = forecastType === "hourly";
|
||||
return [
|
||||
{
|
||||
name: "forecast_type",
|
||||
@@ -62,22 +67,17 @@ export class HuiPrecipitationForecastCardFeatureEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
visible: { field: "forecast_type", value: "hourly" },
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "days_to_show",
|
||||
default: DEFAULT_DAYS_TO_SHOW,
|
||||
visible: {
|
||||
field: "forecast_type",
|
||||
operator: "not_eq",
|
||||
value: "hourly",
|
||||
},
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
isHourly
|
||||
? {
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
}
|
||||
: {
|
||||
name: "days_to_show",
|
||||
default: DEFAULT_DAYS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "precipitation_type",
|
||||
required: true,
|
||||
@@ -141,7 +141,7 @@ export class HuiPrecipitationForecastCardFeatureEditor
|
||||
: { days_to_show: this._config.days_to_show ?? DEFAULT_DAYS_TO_SHOW }),
|
||||
};
|
||||
|
||||
const schema = this._schema(stateObj, this.hass.localize);
|
||||
const schema = this._schema(stateObj, resolvedType, this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
+25
-17
@@ -38,7 +38,8 @@ export class HuiSelectOptionsCardFeatureEditor
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
formatEntityState: FormatEntityStateFunc,
|
||||
stateObj: HassEntity | undefined
|
||||
stateObj: HassEntity | undefined,
|
||||
customizeOptions: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
@@ -47,21 +48,24 @@ export class HuiSelectOptionsCardFeatureEditor
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "options",
|
||||
visible: { field: "customize_options", value: true },
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options:
|
||||
stateObj?.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: formatEntityState(stateObj, option),
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
...(customizeOptions
|
||||
? ([
|
||||
{
|
||||
name: "options",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options:
|
||||
stateObj?.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: formatEntityState(stateObj, option),
|
||||
})) || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
@@ -79,7 +83,11 @@ export class HuiSelectOptionsCardFeatureEditor
|
||||
customize_options: this._config.options !== undefined,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.hass.formatEntityState, stateObj);
|
||||
const schema = this._schema(
|
||||
this.hass.formatEntityState,
|
||||
stateObj,
|
||||
data.customize_options
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -157,6 +157,8 @@ export class HuiStatisticsGraphCardEditor
|
||||
localize: LocalizeFunc,
|
||||
statisticIds: string[] | undefined,
|
||||
metaDatas: StatisticsMetaData[] | undefined,
|
||||
showFitOption: boolean,
|
||||
hiddenLegend: boolean,
|
||||
enableDateSelect: boolean
|
||||
) => {
|
||||
const units = new Set<string>();
|
||||
@@ -289,18 +291,15 @@ export class HuiStatisticsGraphCardEditor
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
visible: {
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "min_y_axis", operator: "exists" },
|
||||
{ field: "max_y_axis", operator: "exists" },
|
||||
],
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(showFitOption
|
||||
? [
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "logarithmic_scale",
|
||||
required: false,
|
||||
@@ -311,16 +310,15 @@ export class HuiStatisticsGraphCardEditor
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
visible: {
|
||||
field: "hide_legend",
|
||||
operator: "not_eq",
|
||||
value: true,
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(!hiddenLegend
|
||||
? [
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -399,6 +397,9 @@ export class HuiStatisticsGraphCardEditor
|
||||
this.hass.localize,
|
||||
this._configEntities,
|
||||
this._metaDatas,
|
||||
this._config!.min_y_axis !== undefined ||
|
||||
this._config!.max_y_axis !== undefined,
|
||||
!!this._config!.hide_legend,
|
||||
!!this._config!.energy_date_selection
|
||||
);
|
||||
const configured_stat_types = this._config!.stat_types
|
||||
|
||||
+18
-18
@@ -38,10 +38,15 @@ export class HuiTemperatureForecastCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: HassEntity | undefined, localize: HomeAssistant["localize"]) => {
|
||||
(
|
||||
stateObj: HassEntity | undefined,
|
||||
forecastType: ForecastResolution,
|
||||
localize: HomeAssistant["localize"]
|
||||
) => {
|
||||
const supportedTypes = stateObj
|
||||
? getSupportedForecastTypes(stateObj)
|
||||
: [];
|
||||
const isHourly = forecastType === "hourly";
|
||||
return [
|
||||
{
|
||||
name: "forecast_type",
|
||||
@@ -62,22 +67,17 @@ export class HuiTemperatureForecastCardFeatureEditor
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
visible: { field: "forecast_type", value: "hourly" },
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "days_to_show",
|
||||
default: DEFAULT_DAYS_TO_SHOW,
|
||||
visible: {
|
||||
field: "forecast_type",
|
||||
operator: "not_eq",
|
||||
value: "hourly",
|
||||
},
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
isHourly
|
||||
? {
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
}
|
||||
: {
|
||||
name: "days_to_show",
|
||||
default: DEFAULT_DAYS_TO_SHOW,
|
||||
selector: { number: { min: 1, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
@@ -117,7 +117,7 @@ export class HuiTemperatureForecastCardFeatureEditor
|
||||
: { days_to_show: this._config.days_to_show ?? DEFAULT_DAYS_TO_SHOW }),
|
||||
};
|
||||
|
||||
const schema = this._schema(stateObj, this.hass.localize);
|
||||
const schema = this._schema(stateObj, resolvedType, this.hass.localize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -145,7 +145,7 @@ export class HuiTileCardEditor
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
visible: { field: "hide_state", operator: "not_eq", value: true },
|
||||
hidden: { field: "hide_state", value: true },
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
@@ -157,7 +157,7 @@ export class HuiTileCardEditor
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: showTimeFormat,
|
||||
hidden: !showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
|
||||
+15
-13
@@ -37,18 +37,20 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne((stateObj: HassEntity | undefined) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
private _schema = memoizeOne(
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -61,7 +63,7 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
: undefined;
|
||||
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj);
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
|
||||
@@ -137,6 +137,7 @@ export class HuiDialogEditSection
|
||||
case "tab-settings":
|
||||
content = html`
|
||||
<hui-section-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.viewConfig=${this._viewConfig}
|
||||
@value-changed=${this._configChanged}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { mdiPalette } from "@mdi/js";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
type LovelaceSectionRawConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
interface SettingsData {
|
||||
column_span?: number;
|
||||
@@ -27,15 +27,14 @@ interface SettingsData {
|
||||
|
||||
@customElement("hui-section-settings-editor")
|
||||
export class HuiDialogEditSection extends LitElement {
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number, localize: LocalizeFunc) =>
|
||||
(maxColumns: number, backgroundEnabled: boolean, localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
@@ -51,45 +50,48 @@ export class HuiDialogEditSection extends LitElement {
|
||||
name: "background_enabled",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "background",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
expanded: true,
|
||||
visible: { field: "background_enabled", value: true },
|
||||
iconPath: mdiPalette,
|
||||
schema: [
|
||||
{
|
||||
name: "background_color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
extra_options: [
|
||||
{
|
||||
value: "default",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.edit_section.settings.background_color_default"
|
||||
),
|
||||
display_color:
|
||||
"var(--ha-section-background-color, var(--secondary-background-color))",
|
||||
...(backgroundEnabled
|
||||
? ([
|
||||
{
|
||||
name: "background",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
expanded: true,
|
||||
iconPath: mdiPalette,
|
||||
schema: [
|
||||
{
|
||||
name: "background_color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
extra_options: [
|
||||
{
|
||||
value: "default",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.edit_section.settings.background_color_default"
|
||||
),
|
||||
display_color:
|
||||
"var(--ha-section-background-color, var(--secondary-background-color))",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "background_opacity",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
unit_of_measurement: "%",
|
||||
mode: "slider",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "background_opacity",
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
unit_of_measurement: "%",
|
||||
mode: "slider",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "theme",
|
||||
selector: {
|
||||
@@ -114,11 +116,13 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
const schema = this._schema(
|
||||
this.viewConfig.max_columns || 4,
|
||||
this._localize
|
||||
backgroundEnabled,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@@ -131,14 +135,14 @@ export class HuiDialogEditSection extends LitElement {
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this._localize(
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this._localize(
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const entitiesConfigStruct = union([
|
||||
image: optional(string()),
|
||||
secondary_info: optional(string()),
|
||||
time_format: optional(timeFormatConfigStruct),
|
||||
color: optional(string()),
|
||||
state_color: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
|
||||
@@ -170,6 +170,7 @@ export class HuiDialogEditView extends DirtyStateProviderMixin<LovelaceViewConfi
|
||||
content = html`
|
||||
<hui-view-editor
|
||||
.isNew=${this._params.viewIndex === undefined}
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
@view-config-changed=${this._viewConfigChanged}
|
||||
></hui-view-editor>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { slugify } from "../../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
@@ -11,6 +10,7 @@ import type {
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
MASONRY_VIEW_LAYOUT,
|
||||
SECTIONS_VIEW_LAYOUT,
|
||||
@@ -33,8 +33,7 @@ const INTEGER_REGEX = /^[0-9]+$/;
|
||||
|
||||
@customElement("hui-view-editor")
|
||||
export class HuiViewEditor extends LitElement {
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public isNew = false;
|
||||
|
||||
@@ -45,7 +44,7 @@ export class HuiViewEditor extends LitElement {
|
||||
private _suggestedPath = false;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
(localize: LocalizeFunc, viewType: string) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@@ -88,41 +87,41 @@ export class HuiViewEditor extends LitElement {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "section_specifics",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
expanded: true,
|
||||
visible: {
|
||||
field: "type",
|
||||
value: SECTIONS_VIEW_LAYOUT,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
name: "max_columns",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
mode: "slider",
|
||||
slider_ticks: true,
|
||||
},
|
||||
...(viewType === SECTIONS_VIEW_LAYOUT
|
||||
? ([
|
||||
{
|
||||
name: "section_specifics",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
expanded: true,
|
||||
schema: [
|
||||
{
|
||||
name: "max_columns",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: 10,
|
||||
mode: "slider",
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dense_section_placement",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "top_margin",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dense_section_placement",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "top_margin",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
@@ -135,6 +134,12 @@ export class HuiViewEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize, this._type);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
type: this._type,
|
||||
@@ -150,8 +155,9 @@ export class HuiViewEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._schema(this._localize)}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.computeError=${this._computeError}
|
||||
@@ -201,14 +207,14 @@ export class HuiViewEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _computeError = (error: string) =>
|
||||
this._localize(`ui.panel.lovelace.editor.edit_view.${error}`) || error;
|
||||
this.hass.localize(`ui.panel.lovelace.editor.edit_view.${error}`) || error;
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "path":
|
||||
return this._localize("ui.panel.lovelace.editor.card.generic.url");
|
||||
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url");
|
||||
case "type":
|
||||
case "show_icon_and_title":
|
||||
case "subview":
|
||||
@@ -216,11 +222,11 @@ export class HuiViewEditor extends LitElement {
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
case "section_specifics":
|
||||
return this._localize(
|
||||
return this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this._localize(
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
@@ -235,7 +241,7 @@ export class HuiViewEditor extends LitElement {
|
||||
case "subview":
|
||||
case "dense_section_placement":
|
||||
case "top_margin":
|
||||
return this._localize(
|
||||
return this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.${schema.name}_helper`
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { applyDefaultColor } from "../common/entity-color-config";
|
||||
import type { EntityCardConfig } from "../cards/types";
|
||||
import { HuiConditionalBase } from "../components/hui-conditional-base";
|
||||
import { createRowElement } from "../create-element/create-row-element";
|
||||
import type { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
|
||||
import type {
|
||||
ConditionalRowConfig,
|
||||
EntityConfig,
|
||||
LovelaceRow,
|
||||
} from "../entity-rows/types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
@@ -20,10 +23,13 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
|
||||
throw new Error("No row configured");
|
||||
}
|
||||
|
||||
const { color } = config as EntitiesCardEntityConfig;
|
||||
|
||||
this._element = createRowElement(
|
||||
applyDefaultColor(config.row as EntitiesCardEntityConfig, color)
|
||||
(config as EntityCardConfig).state_color
|
||||
? ({
|
||||
state_color: true,
|
||||
...(config.row as EntityConfig),
|
||||
} as EntityConfig)
|
||||
: config.row
|
||||
) as LovelaceRow;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class StateCardLock extends LitElement {
|
||||
.inDialog=${this.inDialog}
|
||||
></state-info>
|
||||
${
|
||||
!supportsOpen
|
||||
supportsOpen
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
|
||||
@@ -348,7 +348,6 @@
|
||||
"temperature": "Temperature",
|
||||
"visibility": "Visibility",
|
||||
"wind_speed": "Wind speed",
|
||||
"wind_gust_speed": "Wind gust speed",
|
||||
"precipitation": "Precipitation"
|
||||
},
|
||||
"cardinal_direction": {
|
||||
@@ -1495,7 +1494,6 @@
|
||||
"revert_error": "Failed to revert the configuration: {error}"
|
||||
},
|
||||
"quick-bar": {
|
||||
"ask_assist": "Ask Assist: {query}",
|
||||
"commands_title": "Commands",
|
||||
"navigate_title": "Navigate",
|
||||
"commands": {
|
||||
@@ -2513,11 +2511,7 @@
|
||||
"new_version_available": "A new version of the frontend is available.",
|
||||
"reload": "Reload",
|
||||
"theme_save_failed": "Unable to save theme settings to your user profile.",
|
||||
"theme_preferences_unavailable": "Unable to load user profile theme settings.",
|
||||
"onboarding_survey": {
|
||||
"message": "Hello there! 👋 You've been using Home Assistant for a little while now. We'd love to hear what you think. Just one quick minute!",
|
||||
"action": "Take survey"
|
||||
}
|
||||
"theme_preferences_unavailable": "Unable to load user profile theme settings."
|
||||
},
|
||||
"sidebar": {
|
||||
"external_app_configuration": "App settings",
|
||||
|
||||
@@ -3,27 +3,10 @@ import { render } from "lit";
|
||||
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
|
||||
let removalInitiated = false;
|
||||
|
||||
/**
|
||||
* Removes the launch screen with a fade-out view transition.
|
||||
*
|
||||
* @param instant - Removes the launch screen without animation. Used when the
|
||||
* external app covers the frontend with its own splash screen until the
|
||||
* `frontend/loaded` event, where the animation would play invisibly underneath.
|
||||
* @returns Whether this call initiated the removal (false when the removal
|
||||
* was already initiated, e.g. while the fade-out is still running).
|
||||
*/
|
||||
export const removeLaunchScreen = (instant = false): boolean => {
|
||||
export const removeLaunchScreen = () => {
|
||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||
if (removalInitiated || !launchScreenElement?.parentElement) {
|
||||
return false;
|
||||
}
|
||||
removalInitiated = true;
|
||||
|
||||
if (instant) {
|
||||
launchScreenElement.parentElement.removeChild(launchScreenElement);
|
||||
return true;
|
||||
if (!launchScreenElement?.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
withViewTransition((viewTransitionAvailable: boolean) => {
|
||||
@@ -42,7 +25,6 @@ export const removeLaunchScreen = (instant = false): boolean => {
|
||||
launchScreenElement.parentElement?.removeChild(launchScreenElement);
|
||||
}, parseAnimationDuration(durationFromCss));
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
CoreFrontendSystemData,
|
||||
SurveyInteraction,
|
||||
} from "../data/frontend";
|
||||
import { saveFrontendSystemData } from "../data/frontend";
|
||||
import type { CurrentUser, HomeAssistant } from "../types";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
const SURVEY_MIN_AGE = 5 * 24 * 60 * 60 * 1000; // 5 days
|
||||
const SURVEY_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 1 month
|
||||
|
||||
// Always the production site: the survey is a single page that is not
|
||||
// version-specific (the version is passed as a query parameter instead), so
|
||||
// beta/dev installs should not be routed to rc./next. like documentationUrl
|
||||
// would.
|
||||
const SURVEY_URL = "https://ohf.to/ha/surveys/onboarding";
|
||||
|
||||
export const shouldShowOnboardingSurvey = (
|
||||
user: CurrentUser | undefined,
|
||||
systemData: CoreFrontendSystemData | undefined,
|
||||
now: number = Date.now()
|
||||
): boolean => {
|
||||
if (!user?.is_owner || !systemData?.onboarded_date) {
|
||||
return false;
|
||||
}
|
||||
if (systemData.surveys?.onboarding) {
|
||||
return false;
|
||||
}
|
||||
const age = now - new Date(systemData.onboarded_date).getTime();
|
||||
// NaN (invalid date) and future dates (clock skew) both fail these checks
|
||||
return age >= SURVEY_MIN_AGE && age <= SURVEY_MAX_AGE;
|
||||
};
|
||||
|
||||
export const recordOnboardingSurvey = (
|
||||
conn: Connection,
|
||||
systemData: CoreFrontendSystemData,
|
||||
action: SurveyInteraction["action"]
|
||||
): Promise<void> =>
|
||||
// saveFrontendSystemData overwrites the whole "core" object, so spread the
|
||||
// existing data to preserve the other fields.
|
||||
saveFrontendSystemData(conn, "core", {
|
||||
...systemData,
|
||||
surveys: {
|
||||
...systemData.surveys,
|
||||
onboarding: { date: new Date().toISOString(), action },
|
||||
},
|
||||
});
|
||||
|
||||
export const getOnboardingSurveyUrl = (
|
||||
systemData: CoreFrontendSystemData
|
||||
): string =>
|
||||
systemData.onboarded_version
|
||||
? `${SURVEY_URL}?version=${encodeURIComponent(systemData.onboarded_version)}`
|
||||
: SURVEY_URL;
|
||||
|
||||
export const checkOnboardingSurveyToast = (
|
||||
el: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
) => {
|
||||
if (!shouldShowOnboardingSurvey(hass.user, hass.systemData)) {
|
||||
return;
|
||||
}
|
||||
const record = (action: SurveyInteraction["action"]) =>
|
||||
recordOnboardingSurvey(hass.connection, hass.systemData!, action);
|
||||
showToast(el, {
|
||||
id: "onboarding-survey",
|
||||
message: {
|
||||
translationKey: "ui.notification_toast.onboarding_survey.message",
|
||||
},
|
||||
duration: -1,
|
||||
dismissable: true,
|
||||
action: {
|
||||
text: {
|
||||
translationKey: "ui.notification_toast.onboarding_survey.action",
|
||||
},
|
||||
action: () => {
|
||||
window.open(getOnboardingSurveyUrl(hass.systemData!), "_blank");
|
||||
record("opened");
|
||||
},
|
||||
},
|
||||
dismiss: () => record("dismissed"),
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
|
||||
import { canToggleState } from "../../../src/common/entity/can_toggle_state";
|
||||
import { ClimateEntityFeature } from "../../../src/data/climate";
|
||||
|
||||
describe("canToggleState", () => {
|
||||
const hass: any = {
|
||||
@@ -49,8 +48,7 @@ describe("canToggleState", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "climate.bla",
|
||||
attributes: {
|
||||
supported_features:
|
||||
ClimateEntityFeature.TURN_ON + ClimateEntityFeature.TURN_OFF,
|
||||
supported_features: 4096,
|
||||
},
|
||||
};
|
||||
assert.isTrue(canToggleState(hass, stateObj));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user