mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-24 17:22:48 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 418327d6fe | |||
| e637c0c583 | |||
| 77cef2429b | |||
| b6eb4a50d9 | |||
| 75fded1a43 | |||
| e53ffd76ac | |||
| 54c54fa5a2 | |||
| a4aec3a734 | |||
| c73e735164 | |||
| d4b1fe0c7f | |||
| 8ecd350e6f | |||
| a26de31a2d | |||
| 77110afc59 | |||
| 7b6e9ba738 | |||
| 1b15bc721b | |||
| 93a8d296a8 | |||
| 734fed21a8 | |||
| af203d640f | |||
| da29c8f536 | |||
| 8069596c87 | |||
| ace55fdb92 | |||
| dae8adab98 | |||
| 1522d979de | |||
| 5fd253b2d3 | |||
| f7d5195161 | |||
| b72791a9e2 | |||
| a3c0e8d519 | |||
| 19fcb9d2f7 | |||
| cbd90884ee |
@@ -0,0 +1,244 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ── Build the demo once and share it across test jobs via artifact ──────────
|
||||
build-demo:
|
||||
name: Build demo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload demo build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Build the e2e test app and share it via artifact ────────────────────────
|
||||
build-e2e-test-app:
|
||||
name: Build e2e test app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build e2e test app
|
||||
run: ./node_modules/.bin/gulp build-e2e-test-app
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload e2e test app build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Build the gallery and share it via artifact ─────────────────────────────
|
||||
build-gallery:
|
||||
name: Build gallery
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build gallery
|
||||
run: ./node_modules/.bin/gulp build-gallery
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload gallery build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── 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
|
||||
# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
# Cache the downloaded browser build keyed on the pinned Playwright
|
||||
# version (yarn.lock), so re-runs skip the ~170 MB download.
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: yarn playwright install --with-deps chromium
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
|
||||
- name: Run Playwright tests (local)
|
||||
run: yarn test:e2e
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload blob report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
retention-days: 3
|
||||
|
||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
||||
report:
|
||||
name: Report
|
||||
needs: [e2e-local]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download blob report (local)
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
|
||||
- name: Stage blobs for merge
|
||||
run: node test/e2e/collect-blob-reports.mjs
|
||||
|
||||
- name: Merge blob reports
|
||||
run: npx playwright merge-reports -c test/e2e/playwright.merge.config.ts test/e2e/reports/blob
|
||||
|
||||
- name: Upload merged HTML report
|
||||
id: upload-report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test/e2e/reports/combined/
|
||||
retention-days: 14
|
||||
|
||||
- name: Post report link to PR
|
||||
if: github.event_name == 'pull_request' && needs.e2e-local.result == 'failure'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const body = `## Playwright E2E tests failed\n\nThe combined HTML report is available as a workflow artifact.\n\n[View workflow run](${runUrl})`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
@@ -54,8 +54,16 @@ src/cast/dev_const.ts
|
||||
# test coverage
|
||||
test/coverage/
|
||||
|
||||
# Playwright e2e output
|
||||
test/e2e/reports/
|
||||
test/e2e/test-results/
|
||||
# E2E test app build output
|
||||
test/e2e/app/dist/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
.cursor
|
||||
.opencode
|
||||
.serena
|
||||
|
||||
test/benchmarks/results/
|
||||
|
||||
+36
-34
@@ -1,4 +1,3 @@
|
||||
/* global require, module, __dirname, process */
|
||||
const path = require("path");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
@@ -84,12 +83,7 @@ module.exports.swcOptions = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.babelOptions = ({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw,
|
||||
}) => ({
|
||||
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
assumptions: {
|
||||
@@ -102,14 +96,22 @@ module.exports.babelOptions = ({
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: "usage",
|
||||
corejs: dependencies["core-js"],
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
// Inject Core-JS polyfills on demand. Babel 8 removed preset-env's
|
||||
// `useBuiltIns`/`corejs` options, so the equivalent polyfill provider is
|
||||
// configured directly here (`usage-global` matches the old `useBuiltIns: "usage"`).
|
||||
[
|
||||
"babel-plugin-polyfill-corejs3",
|
||||
{
|
||||
method: "usage-global",
|
||||
version: dependencies["core-js"],
|
||||
shippedProposals: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
||||
{
|
||||
@@ -117,32 +119,14 @@ module.exports.babelOptions = ({
|
||||
ignoreModuleNotFound: true,
|
||||
},
|
||||
],
|
||||
// Minify template literals for production
|
||||
isProdBuild && [
|
||||
"template-html-minifier",
|
||||
{
|
||||
modules: {
|
||||
...Object.fromEntries(
|
||||
["lit", "lit-element", "lit-html"].map((m) => [
|
||||
m,
|
||||
[
|
||||
"html",
|
||||
{ name: "svg", encapsulation: "svg" },
|
||||
{ name: "css", encapsulation: "style" },
|
||||
],
|
||||
])
|
||||
),
|
||||
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
|
||||
},
|
||||
strictCSS: true,
|
||||
htmlMinifier: module.exports.htmlMinifierOptions,
|
||||
failOnError: false, // we can turn this off in case of false positives
|
||||
},
|
||||
],
|
||||
// Import helpers and regenerator from runtime package
|
||||
// Import helpers and regenerator from runtime package.
|
||||
// `moduleName` is pinned so helpers resolve from `@babel/runtime`: the
|
||||
// corejs3 polyfill provider above otherwise redirects them to the
|
||||
// (uninstalled) `@babel/runtime-corejs3`, which preset-env used to suppress
|
||||
// internally when it owned the polyfill injection via `useBuiltIns`.
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{ version: dependencies["@babel/runtime"] },
|
||||
{ version: dependencies["@babel/runtime"], moduleName: "@babel/runtime" },
|
||||
],
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-private-methods",
|
||||
@@ -321,4 +305,22 @@ module.exports.config = {
|
||||
isLandingPageBuild: true,
|
||||
};
|
||||
},
|
||||
|
||||
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||
return {
|
||||
name: "e2e-test-app" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
main: path.resolve(paths.e2eTestApp_dir, "src/entrypoint.ts"),
|
||||
},
|
||||
outputPath: outputPath(paths.e2eTestApp_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild),
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`E2E-TEST-${env.version()}`),
|
||||
__DEMO__: true,
|
||||
},
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// @ts-check
|
||||
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global module */
|
||||
// Browser-only replacement for core-js/internals/get-built-in-node-module.
|
||||
// The original helper evaluates `Function('return require("...")')()`
|
||||
// when it detects a Node environment, which causes a runtime
|
||||
|
||||
@@ -45,3 +45,10 @@ gulp.task(
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-e2e-test-app",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.e2eTestApp_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-e2e-test-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"gen-pages-e2e-test-app-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-e2e-test-app",
|
||||
"rspack-dev-server-e2e-test-app"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-e2e-test-app",
|
||||
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",
|
||||
"gen-pages-e2e-test-app-prod"
|
||||
)
|
||||
);
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global process */
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import {
|
||||
@@ -268,3 +267,24 @@ gulp.task(
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const E2E_TEST_APP_PAGE_ENTRIES = { "index.html": ["main"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-e2e-test-app-dev",
|
||||
genPagesDevTask(
|
||||
E2E_TEST_APP_PAGE_ENTRIES,
|
||||
paths.e2eTestApp_dir,
|
||||
paths.e2eTestApp_output_root
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-e2e-test-app-prod",
|
||||
genPagesProdTask(
|
||||
E2E_TEST_APP_PAGE_ENTRIES,
|
||||
paths.e2eTestApp_dir,
|
||||
paths.e2eTestApp_output_root,
|
||||
paths.e2eTestApp_output_latest
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import yaml from "js-yaml";
|
||||
import { load as loadYaml } from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
@@ -47,7 +47,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
|
||||
if (descriptionContent.startsWith("---")) {
|
||||
const metadataEnd = descriptionContent.indexOf("---", 3);
|
||||
metadata = yaml.load(descriptionContent.substring(3, metadataEnd));
|
||||
metadata = loadYaml(descriptionContent.substring(3, metadataEnd));
|
||||
descriptionContent = descriptionContent
|
||||
.substring(metadataEnd + 3)
|
||||
.trim();
|
||||
|
||||
@@ -201,3 +201,23 @@ gulp.task("copy-static-landing-page", async () => {
|
||||
copyFonts(paths.landingPage_output_static);
|
||||
copyTranslations(paths.landingPage_output_static);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-e2e-test-app", async () => {
|
||||
// Copy app static files (icons, polyfills, etc.)
|
||||
fs.copySync(
|
||||
polyPath("public/static"),
|
||||
path.resolve(paths.e2eTestApp_output_root, "static")
|
||||
);
|
||||
// Copy e2e test app public files (manifest, sw stubs)
|
||||
const e2ePublic = path.resolve(paths.e2eTestApp_dir, "public");
|
||||
if (fs.existsSync(e2ePublic)) {
|
||||
fs.copySync(e2ePublic, paths.e2eTestApp_output_root);
|
||||
}
|
||||
|
||||
copyPolyfills(paths.e2eTestApp_output_static);
|
||||
copyMapPanel(paths.e2eTestApp_output_static);
|
||||
copyFonts(paths.e2eTestApp_output_static);
|
||||
copyTranslations(paths.e2eTestApp_output_static);
|
||||
copyLocaleData(paths.e2eTestApp_output_static);
|
||||
copyMdiIcons(paths.e2eTestApp_output_static);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./demo.js";
|
||||
import "./download-translations.js";
|
||||
import "./e2e-test-app.js";
|
||||
import "./entry-html.js";
|
||||
import "./fetch-nightly-translations.js";
|
||||
import "./gallery.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Gulp task to generate third-party license notices.
|
||||
|
||||
import { readFile, access } from "fs/promises";
|
||||
import { readFile, access, readdir } from "fs/promises";
|
||||
import { generateLicenseFile } from "generate-license-file";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
@@ -11,58 +11,98 @@ const OUTPUT_FILE = path.join(
|
||||
"third-party-licenses.txt"
|
||||
);
|
||||
|
||||
const NODE_MODULES = path.resolve(paths.root_dir, "node_modules");
|
||||
|
||||
// The echarts package ships an Apache-2.0 NOTICE file that must be
|
||||
// redistributed alongside the compiled output per Apache License §4(d).
|
||||
const NOTICE_FILES = [
|
||||
path.resolve(paths.root_dir, "node_modules/echarts/NOTICE"),
|
||||
];
|
||||
const NOTICE_FILES = [path.join(NODE_MODULES, "echarts/NOTICE")];
|
||||
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers the bundled code.
|
||||
// Some packages need a manual license override (e.g. they ship multiple
|
||||
// license files and we must pick the right one for the bundled code).
|
||||
//
|
||||
// Each entry is pinned to a specific version. If a package is updated,
|
||||
// this list must be reviewed and the version updated after verifying
|
||||
// that the new version's license still matches. The build will fail
|
||||
// if the installed version does not match the pinned version.
|
||||
// that the new version's license still matches. The build will fail if
|
||||
// the pinned version is no longer installed.
|
||||
const LICENSE_OVERRIDES = [
|
||||
{
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers the bundled code.
|
||||
packageName: "type-fest",
|
||||
version: "5.7.0",
|
||||
licensePath: path.resolve(
|
||||
paths.root_dir,
|
||||
"node_modules/type-fest/license-mit"
|
||||
),
|
||||
licenseFile: "license-mit",
|
||||
},
|
||||
];
|
||||
|
||||
// Locate the directory of an installed package matching an exact version.
|
||||
//
|
||||
// The copy we care about may be hoisted to the top-level node_modules or
|
||||
// nested under a dependency when a different version occupies the hoisted
|
||||
// slot (e.g. a build-only dependency pulling in an older release). Searching
|
||||
// both keeps this check independent of yarn's hoisting decisions, which can
|
||||
// shift when unrelated dependencies are added.
|
||||
async function findPackageDir(packageName, version) {
|
||||
const candidateDirs = [path.join(NODE_MODULES, packageName)];
|
||||
|
||||
// Collect one level of nesting: node_modules/<dep>/node_modules/<pkg> and
|
||||
// node_modules/@scope/<dep>/node_modules/<pkg>.
|
||||
let topLevel = [];
|
||||
try {
|
||||
topLevel = await readdir(NODE_MODULES, { withFileTypes: true });
|
||||
} catch {
|
||||
// node_modules unreadable — fall back to the hoisted candidate only.
|
||||
}
|
||||
for (const entry of topLevel) {
|
||||
if (!entry.isDirectory() || entry.name === packageName) {
|
||||
continue;
|
||||
}
|
||||
if (entry.name.startsWith("@")) {
|
||||
const scopeDir = path.join(NODE_MODULES, entry.name);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const scoped = await readdir(scopeDir, { withFileTypes: true }).catch(
|
||||
() => []
|
||||
);
|
||||
for (const dep of scoped) {
|
||||
if (dep.isDirectory()) {
|
||||
candidateDirs.push(
|
||||
path.join(scopeDir, dep.name, "node_modules", packageName)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidateDirs.push(
|
||||
path.join(NODE_MODULES, entry.name, "node_modules", packageName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const dir of candidateDirs) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const pkg = await readFile(path.join(dir, "package.json"), "utf-8")
|
||||
.then(JSON.parse)
|
||||
.catch(() => null);
|
||||
if (pkg?.version === version) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
gulp.task("gen-licenses", async () => {
|
||||
const licenseOverrides = {};
|
||||
|
||||
for (const { packageName, version, licensePath } of LICENSE_OVERRIDES) {
|
||||
const pkgJsonPath = path.resolve(
|
||||
paths.root_dir,
|
||||
`node_modules/${packageName}/package.json`
|
||||
);
|
||||
for (const { packageName, version, licenseFile } of LICENSE_OVERRIDES) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const packageDir = await findPackageDir(packageName, version);
|
||||
|
||||
let packageJSON;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJSON = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
||||
} catch {
|
||||
if (!packageDir) {
|
||||
throw new Error(
|
||||
`package.json for "${packageName}" not found or unreadable at ${pkgJsonPath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (packageJSON.version !== version) {
|
||||
throw new Error(
|
||||
`License override for "${packageName}" is pinned to version ${version}, but found version ${packageJSON.version}. ` +
|
||||
`License override for "${packageName}" is pinned to version ${version}, but that version is not installed. ` +
|
||||
`Please verify the new version's license and update the override in build-scripts/gulp/licenses.js.`
|
||||
);
|
||||
}
|
||||
|
||||
const licensePath = path.join(packageDir, licenseFile);
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await access(licensePath);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
@@ -231,3 +232,22 @@ gulp.task("rspack-prod-landing-page", () =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-e2e-test-app", () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
createE2eTestAppConfig({ isProdBuild: false, latestBuild: true })
|
||||
),
|
||||
contentBase: paths.e2eTestApp_output_root,
|
||||
port: 8095,
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("rspack-prod-e2e-test-app", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createE2eTestAppConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -48,6 +48,12 @@ for (const buildType of ["Modern", "Legacy"]) {
|
||||
const browserslistEnv = buildType.toLowerCase();
|
||||
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
|
||||
const presetEnvOpts = babelOpts.presets[0][1];
|
||||
// Core-JS polyfills are injected by babel-plugin-polyfill-corejs3 (Babel 8
|
||||
// removed preset-env's `useBuiltIns`), so read its options here.
|
||||
const corejsOpts = babelOpts.plugins.find(
|
||||
(plugin) =>
|
||||
Array.isArray(plugin) && plugin[0] === "babel-plugin-polyfill-corejs3"
|
||||
)?.[1];
|
||||
|
||||
// Invoking preset-env in debug mode will log the included plugins
|
||||
console.log(detailsOpen(`${buildType} Build Babel Plugins`));
|
||||
@@ -59,16 +65,16 @@ for (const buildType of ["Modern", "Legacy"]) {
|
||||
console.log(detailsClose);
|
||||
|
||||
// Manually log the Core-JS polyfills using the same technique
|
||||
if (presetEnvOpts.useBuiltIns) {
|
||||
if (corejsOpts) {
|
||||
console.log(detailsOpen(`${buildType} Build Core-JS Polyfills`));
|
||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
||||
browserslistEnv,
|
||||
});
|
||||
const polyfillList = coreJSCompat({ targets }).list.filter(
|
||||
polyfillFilter(
|
||||
`${presetEnvOpts.useBuiltIns}-global`,
|
||||
presetEnvOpts?.corejs?.proposals,
|
||||
presetEnvOpts?.shippedProposals
|
||||
corejsOpts.method,
|
||||
corejsOpts.proposals,
|
||||
corejsOpts.shippedProposals
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* global module */
|
||||
|
||||
module.exports = function litDisableDevModeLoader(source) {
|
||||
return source.replace(
|
||||
/\b(const|let|var) DEV_MODE = true;/g,
|
||||
"$1 DEV_MODE = false;"
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/* global module, require */
|
||||
// rspack/webpack loader that minifies the HTML, SVG, and CSS inside lit
|
||||
// tagged template literals using `minify-literals` (html-minifier-next +
|
||||
// lightningcss). Replaces the unmaintained babel-plugin-template-html-minifier.
|
||||
//
|
||||
// It runs between swc and babel: swc has already stripped TS types and
|
||||
// decorators (so minify-literals' acorn parser only sees plain ESM), but the
|
||||
// `html`/`css`/`svg` tagged templates are still intact at ES2021. Running after
|
||||
// babel instead would miss the legacy build, where babel lowers the templates
|
||||
// to `_taggedTemplateLiteral()` calls that no longer look like tagged templates.
|
||||
|
||||
const remapping = require("@ampproject/remapping");
|
||||
|
||||
// minify-literals is ESM-only, so load it via dynamic import from this CJS loader.
|
||||
let minifyPromise;
|
||||
const getMinifier = () => {
|
||||
if (!minifyPromise) {
|
||||
minifyPromise = import("minify-literals").then((m) => m.minifyHTMLLiterals);
|
||||
}
|
||||
return minifyPromise;
|
||||
};
|
||||
|
||||
// HTML options mirror the previous babel-plugin-template-html-minifier config
|
||||
// (html-minifier-next is option-compatible with html-minifier-terser). CSS in
|
||||
// css`` templates and inline <style> is handled by minify-literals' lightningcss
|
||||
// default.
|
||||
const htmlOptions = {
|
||||
caseSensitive: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
decodeEntities: true,
|
||||
removeComments: true,
|
||||
removeRedundantAttributes: true,
|
||||
};
|
||||
|
||||
module.exports = function minifyTemplateLiteralsLoader(source, map, meta) {
|
||||
const callback = this.async();
|
||||
getMinifier()
|
||||
.then((minifyHTMLLiterals) =>
|
||||
minifyHTMLLiterals(source, {
|
||||
fileName: this.resourcePath,
|
||||
html: htmlOptions,
|
||||
})
|
||||
)
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
// No tagged templates changed; pass through untouched (incl. incoming map).
|
||||
callback(null, source, map, meta);
|
||||
return;
|
||||
}
|
||||
// minify-literals builds its map from `source` alone, so `result.map`
|
||||
// describes minified output -> this loader's input (the swc output), not
|
||||
// the original file. Compose it over the incoming map (swc output ->
|
||||
// original source) so the map handed downstream still points at the
|
||||
// original source; otherwise every minified file's source map is wrong.
|
||||
const outMap =
|
||||
map && result.map
|
||||
? remapping([result.map, map], () => null)
|
||||
: (result.map ?? map);
|
||||
callback(null, result.code, outMap, meta);
|
||||
})
|
||||
.catch(callback);
|
||||
};
|
||||
@@ -50,4 +50,15 @@ module.exports = {
|
||||
),
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
|
||||
e2eTestApp_dir: path.resolve(__dirname, "../test/e2e/app"),
|
||||
e2eTestApp_output_root: path.resolve(__dirname, "../test/e2e/app/dist"),
|
||||
e2eTestApp_output_static: path.resolve(
|
||||
__dirname,
|
||||
"../test/e2e/app/dist/static"
|
||||
),
|
||||
e2eTestApp_output_latest: path.resolve(
|
||||
__dirname,
|
||||
"../test/e2e/app/dist/frontend_latest"
|
||||
),
|
||||
};
|
||||
|
||||
+88
-19
@@ -1,4 +1,3 @@
|
||||
/* global require, module, __dirname */
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
@@ -48,6 +47,12 @@ const createRspackConfig = ({
|
||||
dontHash = new Set();
|
||||
}
|
||||
const ignorePackages = bundle.ignorePackages({ latestBuild });
|
||||
const litHtmlRoot = path.resolve(__dirname, "../node_modules/lit-html");
|
||||
const litHtmlDevelopmentRoot = path.join(litHtmlRoot, "development");
|
||||
const litDisableDevModeLoader = path.join(
|
||||
__dirname,
|
||||
"lit-disable-dev-mode-loader.cjs"
|
||||
);
|
||||
return {
|
||||
name,
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
@@ -67,25 +72,42 @@ const createRspackConfig = ({
|
||||
{
|
||||
test: /\.m?js$|\.ts$/,
|
||||
exclude: /node_modules[\\/]core-js/,
|
||||
use: (info) => [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw: info.issuerLayer === "sw",
|
||||
}),
|
||||
cacheDirectory: !isProdBuild,
|
||||
cacheCompression: false,
|
||||
use: (info) =>
|
||||
[
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({
|
||||
latestBuild,
|
||||
isTestBuild,
|
||||
sw: info.issuerLayer === "sw",
|
||||
}),
|
||||
cacheDirectory: !isProdBuild,
|
||||
cacheCompression: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: "builtin:swc-loader",
|
||||
options: bundle.swcOptions(),
|
||||
},
|
||||
],
|
||||
// Minify lit html/svg/css tagged template literals for production.
|
||||
// Must run after swc (TS/decorators stripped, but templates kept at
|
||||
// ES2021) and before babel — otherwise the legacy build lowers
|
||||
// html`` to _taggedTemplateLiteral() calls that can no longer be
|
||||
// matched, leaving legacy templates unminified.
|
||||
isProdBuild && {
|
||||
loader: path.join(
|
||||
__dirname,
|
||||
"minify-template-literals-loader.cjs"
|
||||
),
|
||||
},
|
||||
!latestBuild &&
|
||||
info.resource.startsWith(
|
||||
`${litHtmlDevelopmentRoot}${path.sep}`
|
||||
) && {
|
||||
loader: litDisableDevModeLoader,
|
||||
},
|
||||
{
|
||||
loader: "builtin:swc-loader",
|
||||
options: bundle.swcOptions(),
|
||||
},
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
@@ -132,6 +154,47 @@ const createRspackConfig = ({
|
||||
// Only include the JS of entrypoints
|
||||
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
||||
}),
|
||||
// Babel can miscompile Lit's pre-minified runtime when downleveling to
|
||||
// ES5. Compile lit-html from its development sources for legacy builds,
|
||||
// then let the normal production minifier handle the final bundle.
|
||||
!latestBuild &&
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
/^(?:lit-html(?:\/.*)?|\.{1,2}\/.*\.js)$/,
|
||||
(resource) => {
|
||||
if (resource.request === "lit-html") {
|
||||
resource.request = path.join(
|
||||
litHtmlDevelopmentRoot,
|
||||
"lit-html.js"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (resource.request.startsWith("lit-html/")) {
|
||||
if (resource.request.startsWith("lit-html/development/")) {
|
||||
return;
|
||||
}
|
||||
resource.request = path.join(
|
||||
litHtmlDevelopmentRoot,
|
||||
resource.request.slice("lit-html/".length)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
resource.context.startsWith(`${litHtmlRoot}${path.sep}`) &&
|
||||
resource.context !== litHtmlDevelopmentRoot &&
|
||||
!resource.context.startsWith(
|
||||
`${litHtmlDevelopmentRoot}${path.sep}`
|
||||
)
|
||||
) {
|
||||
resource.request = path.join(
|
||||
litHtmlDevelopmentRoot,
|
||||
path.relative(
|
||||
litHtmlRoot,
|
||||
path.resolve(resource.context, resource.request)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
new rspack.DefinePlugin(
|
||||
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
|
||||
),
|
||||
@@ -338,6 +401,11 @@ const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||
|
||||
const createE2eTestAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
createRspackConfig(
|
||||
bundle.config.e2eTestApp({ isProdBuild, latestBuild, isStatsBuild })
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
@@ -345,4 +413,5 @@ module.exports = {
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAssist = (hass: MockHomeAssistant) => {
|
||||
// Stub for assist pipeline list — returns empty so developer tools assist
|
||||
// tab loads without errors.
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines: [],
|
||||
preferred_pipeline: null,
|
||||
}));
|
||||
|
||||
// Stub for assist pipeline run — immediately sends run-end event so
|
||||
// the UI does not hang waiting for a response.
|
||||
hass.mockWS("assist_pipeline/run", (_msg, _hass, onChange) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
type: "run-end",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockUpdate = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("update/list", () => []);
|
||||
};
|
||||
@@ -234,6 +234,12 @@ export default tseslint.config(
|
||||
globals: globals.serviceworker,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["test/e2e/*.mjs"],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -248,7 +248,7 @@ class DemoThermostatEntity extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
const SHADOWS = ["s", "m", "l"] as const;
|
||||
@@ -17,7 +18,9 @@ export class DemoMiscBoxShadow extends LitElement {
|
||||
(size) => html`
|
||||
<div
|
||||
class="box"
|
||||
style="box-shadow: var(--ha-box-shadow-${size})"
|
||||
style=${styleMap({
|
||||
boxShadow: `var(--ha-box-shadow-${size})`,
|
||||
})}
|
||||
>
|
||||
${size}
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,7 @@ class DemoMoreInfoClimate extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class DemoMoreInfoHumidifier extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
+19
-11
@@ -22,13 +22,18 @@
|
||||
"postpack": "pinst --enable",
|
||||
"test": "vitest run --config test/vitest.config.ts",
|
||||
"test:bench": "vitest bench --run --config test/vitest.bench.config.ts",
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage",
|
||||
"test:e2e": "node test/e2e/run-suites.mjs demo app gallery",
|
||||
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
|
||||
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
@@ -97,7 +102,7 @@
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.5",
|
||||
"intl-messageformat": "11.2.8",
|
||||
"js-yaml": "4.2.0",
|
||||
"js-yaml": "5.0.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -127,10 +132,11 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.7",
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@babel/core": "8.0.1",
|
||||
"@babel/helper-define-polyfill-provider": "1.0.0",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@babel/plugin-transform-runtime": "8.0.1",
|
||||
"@babel/preset-env": "8.0.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.62.0",
|
||||
@@ -138,26 +144,27 @@
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.60.0",
|
||||
"@rsdoctor/rspack-plugin": "1.5.15",
|
||||
"@rspack/core": "2.0.8",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-draw": "1.0.13",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/luxon": "3.7.2",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
@@ -182,11 +189,12 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.7",
|
||||
"lint-staged": "17.0.8",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
"map-stream": "0.0.7",
|
||||
"minify-literals": "2.0.2",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.4",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
@@ -216,6 +224,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.17.0",
|
||||
"volta": {
|
||||
"node": "24.17.0"
|
||||
"node": "24.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,15 @@ export interface EntityPickerDisplay {
|
||||
}
|
||||
|
||||
export const computeEntityPickerDisplay = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<
|
||||
HomeAssistant,
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "areas"
|
||||
| "floors"
|
||||
| "language"
|
||||
| "translationMetadata"
|
||||
>,
|
||||
stateObj: HassEntity
|
||||
): EntityPickerDisplay => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
@@ -8,14 +8,30 @@ export interface RelatedIdSets {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* Build a set of related IDs, merging in the current (queried) item.
|
||||
* `search/related` does not echo the queried item back, but it is the closest
|
||||
* related item (e.g. a card editor's own entity), so it is merged into the
|
||||
* matching group when it is an area, device, or entity.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
* @param current - The queried item to merge in.
|
||||
* @returns The related ID sets, including the current item.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
export const buildRelatedIdSets = (
|
||||
related?: RelatedResult,
|
||||
current?: { itemType: ItemType; itemId: string }
|
||||
): RelatedIdSets => ({
|
||||
areas: new Set([
|
||||
...(related?.area || []),
|
||||
...(current?.itemType === "area" ? [current.itemId] : []),
|
||||
]),
|
||||
devices: new Set([
|
||||
...(related?.device || []),
|
||||
...(current?.itemType === "device" ? [current.itemId] : []),
|
||||
]),
|
||||
entities: new Set([
|
||||
...(related?.entity || []),
|
||||
...(current?.itemType === "entity" ? [current.itemId] : []),
|
||||
]),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1463,6 +1463,12 @@ export class HaDataTable extends LitElement {
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
@media (min-width: 871px) {
|
||||
ha-input-search {
|
||||
--ha-input-search-height: 32px;
|
||||
--ha-input-search-border-radius: 10px;
|
||||
}
|
||||
}
|
||||
slot[name="header"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ import {
|
||||
} from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../ha-sortable";
|
||||
import "./ha-entity-picker";
|
||||
import type { HaEntityPicker } from "./ha-entity-picker";
|
||||
|
||||
@customElement("ha-entities-picker")
|
||||
class HaEntitiesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -87,10 +85,6 @@ class HaEntitiesPicker extends LitElement {
|
||||
public reorder = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentEntities = this._currentEntities;
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
@@ -105,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
|
||||
<div class="entity">
|
||||
<ha-entity-picker
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
@@ -133,7 +126,6 @@ class HaEntitiesPicker extends LitElement {
|
||||
</ha-sortable>
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -6,10 +7,21 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
relatedContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
markEntitiesRelated,
|
||||
sortEntitiesByRelatedRank,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity/entity_picker";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -33,7 +45,21 @@ const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
@@ -131,12 +157,25 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
private get _hasRelatedContext(): boolean {
|
||||
const related = this._relatedIdSets;
|
||||
return (
|
||||
!!related &&
|
||||
(related.entities.size > 0 ||
|
||||
related.devices.size > 0 ||
|
||||
related.areas.size > 0)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states !== changedProperties.get("hass")?.states &&
|
||||
this.hass.states[this._pendingEntityId]
|
||||
changedProperties.has("_states") &&
|
||||
this._states[this._pendingEntityId]
|
||||
) {
|
||||
this._setValue(this._pendingEntityId);
|
||||
this._pendingEntityId = undefined;
|
||||
@@ -146,7 +185,7 @@ export class HaEntityPicker extends LitElement {
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
// Load title translations so it is available when the combo-box opens
|
||||
this.hass.loadBackendTranslation("title");
|
||||
this._i18n.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _findExtraOption(value: string | undefined) {
|
||||
@@ -157,7 +196,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _renderExtraOptionStart(extraOption: EntitySelectorExtraOption) {
|
||||
const stateObj = extraOption.entity_id
|
||||
? this.hass.states[extraOption.entity_id]
|
||||
? this._states[extraOption.entity_id]
|
||||
: undefined;
|
||||
if (stateObj) {
|
||||
return html`
|
||||
@@ -193,7 +232,7 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const stateObj = this._states[entityId];
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
@@ -207,7 +246,11 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
{
|
||||
...this._registries,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
stateObj
|
||||
);
|
||||
|
||||
@@ -219,7 +262,7 @@ export class HaEntityPicker extends LitElement {
|
||||
};
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||
return this.showEntityId || this._config.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
|
||||
@@ -267,17 +310,14 @@ export class HaEntityPicker extends LitElement {
|
||||
};
|
||||
|
||||
private _getAdditionalItems = () =>
|
||||
this._getCreateItems(this.hass.localize, this.createDomains);
|
||||
this._getCreateItems(this._i18n.localize, this.createDomains);
|
||||
|
||||
private _getCreateItems = memoizeOne(
|
||||
(
|
||||
localize: this["hass"]["localize"],
|
||||
createDomains: this["createDomains"]
|
||||
) => {
|
||||
(localize: LocalizeFunc, createDomains: this["createDomains"]) => {
|
||||
if (!createDomains?.length) {
|
||||
return [];
|
||||
}
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
return createDomains.map((domain) => {
|
||||
const primary = localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
@@ -302,7 +342,9 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
states: ContextType<typeof statesContext>,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
@@ -312,21 +354,46 @@ export class HaEntityPicker extends LitElement {
|
||||
excludeEntities?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
})
|
||||
getEntities(
|
||||
{
|
||||
states,
|
||||
...registries,
|
||||
language: i18n.language,
|
||||
translationMetadata: i18n.translationMetadata,
|
||||
localize: i18n.localize,
|
||||
},
|
||||
{
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
private _sortByRelatedContext = memoizeOne(
|
||||
(
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
language: string
|
||||
): EntityComboBoxItem[] =>
|
||||
sortEntitiesByRelatedRank(
|
||||
markEntitiesRelated(items, related, entities, devices),
|
||||
language
|
||||
)
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
const entityItems = this._getEntitiesMemoized(
|
||||
this._states,
|
||||
this._registries,
|
||||
this._i18n,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
@@ -336,14 +403,23 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities,
|
||||
this.value
|
||||
);
|
||||
const sortedItems = this._hasRelatedContext
|
||||
? this._sortByRelatedContext(
|
||||
entityItems,
|
||||
this._relatedIdSets!,
|
||||
this._registries.entities,
|
||||
this._registries.devices,
|
||||
this._i18n.locale.language
|
||||
)
|
||||
: entityItems;
|
||||
if (this.extraOptions?.length) {
|
||||
const resolvedExtras = this.extraOptions.map((opt) => ({
|
||||
...opt,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
stateObj: opt.entity_id ? this._states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...items];
|
||||
return [...resolvedExtras, ...sortedItems];
|
||||
}
|
||||
return items;
|
||||
return sortedItems;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
@@ -353,11 +429,10 @@ export class HaEntityPicker extends LitElement {
|
||||
protected render() {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||
this._i18n.localize("ui.components.entity.entity-picker.placeholder");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
@@ -375,12 +450,13 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.noSort=${this._hasRelatedContext}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
this.hass.localize("ui.components.entity.entity-picker.add"))
|
||||
this._i18n.localize("ui.components.entity.entity-picker.add"))
|
||||
: undefined}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -393,17 +469,23 @@ export class HaEntityPicker extends LitElement {
|
||||
search,
|
||||
filteredItems
|
||||
) => {
|
||||
// Float related items to the top by closeness, keeping search relevance
|
||||
// order within each tier.
|
||||
const items = this._hasRelatedContext
|
||||
? sortEntitiesByRelatedRank(filteredItems)
|
||||
: filteredItems;
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex(
|
||||
const index = items.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
return items;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
const [exactMatch] = items.splice(index, 1);
|
||||
items.unshift(exactMatch);
|
||||
return items;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
@@ -427,7 +509,7 @@ export class HaEntityPicker extends LitElement {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) {
|
||||
if (this.hass.states[item.entityId]) {
|
||||
if (this._states[item.entityId]) {
|
||||
this._setValue(item.entityId);
|
||||
} else {
|
||||
this._pendingEntityId = item.entityId;
|
||||
@@ -453,7 +535,7 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
||||
this._i18n.localize("ui.components.entity.entity-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -226,7 +225,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { areasContext, floorsContext } from "../data/context";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
@@ -17,7 +18,13 @@ export interface AreasDisplayValue {
|
||||
|
||||
@customElement("ha-areas-display-editor")
|
||||
export class HaAreasDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -35,10 +42,10 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areas = Object.values(this.hass.areas);
|
||||
const areas = Object.values(this._areas);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const { floor } = getAreaContext(area, this._floors);
|
||||
return {
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { areasContext, floorsContext } from "../data/context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -30,7 +34,17 @@ const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
@customElement("ha-areas-floors-display-editor")
|
||||
export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -51,13 +65,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const groupedAreasItems = this._groupedAreasItems(
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
this._areas,
|
||||
this._floors
|
||||
);
|
||||
|
||||
const filteredFloors = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).filter(
|
||||
(floor) =>
|
||||
// Only include floors that have areas assigned to them
|
||||
@@ -124,15 +139,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _groupedAreasItems = memoizeOne(
|
||||
(
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
// update items if floors change
|
||||
_hassFloors: HomeAssistant["floors"]
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>
|
||||
): Record<string, DisplayItem[]> => {
|
||||
const areas = Object.values(hassAreas);
|
||||
const areaList = Object.values(areas);
|
||||
|
||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||
const groupedItems: Record<string, DisplayItem[]> = areaList.reduce(
|
||||
(acc, area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const { floor } = getAreaContext(area, floors);
|
||||
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
|
||||
if (!acc[floorId]) {
|
||||
@@ -155,23 +169,24 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _sortedFloors = memoizeOne(
|
||||
(
|
||||
hassFloors: HomeAssistant["floors"],
|
||||
order: string[] | undefined
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
order: string[] | undefined,
|
||||
localize: LocalizeFunc
|
||||
): FloorRegistryEntry[] => {
|
||||
const floors = getFloors(hassFloors, order);
|
||||
const noFloors = floors.length === 0;
|
||||
floors.push({
|
||||
const sortedFloors = getFloors(floors, order);
|
||||
const noFloors = sortedFloors.length === 0;
|
||||
sortedFloors.push({
|
||||
floor_id: UNASSIGNED_FLOOR,
|
||||
name: noFloors
|
||||
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
? localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
return floors;
|
||||
return sortedFloors;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -180,8 +195,9 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).map((floor) => floor.floor_id);
|
||||
const newOrder = [...floorIds];
|
||||
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
|
||||
@@ -204,8 +220,9 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
const floorIds = this._sortedFloors(
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
).map((floor) => floor.floor_id);
|
||||
|
||||
const oldAreaDisplay = this.value?.areas_display ?? {};
|
||||
@@ -223,14 +240,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
const order = oldOrder.filter((areaId) => {
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (order?.length) {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeEntityStates } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import { entityIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
|
||||
@@ -15,7 +23,21 @@ export interface EntitiesDisplayValue {
|
||||
|
||||
@customElement("ha-entities-display-editor")
|
||||
export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeEntityStates({ entityIdPath: ["entitiesIds"] })
|
||||
private _entityStates?: Record<string, HassEntity>;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
@state()
|
||||
private _entitiesReg!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@state()
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@state()
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -32,20 +54,13 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const entities = this.entitiesIds
|
||||
.map((entityId) => this.hass.states[entityId])
|
||||
.filter(Boolean);
|
||||
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
const items = this._items(
|
||||
this.entitiesIds,
|
||||
this._entityStates,
|
||||
this._entitiesReg,
|
||||
this._config,
|
||||
this._connection
|
||||
);
|
||||
|
||||
const value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
@@ -61,6 +76,31 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
entitiesIds: string[],
|
||||
entityStates: Record<string, HassEntity> | undefined,
|
||||
entitiesReg: ContextType<typeof entitiesContext>,
|
||||
config: ContextType<typeof configContext>,
|
||||
connection: ContextType<typeof connectionContext>
|
||||
): DisplayItem[] => {
|
||||
const entities = entitiesIds
|
||||
.map((entityId) => entityStates?.[entityId])
|
||||
.filter((stateObj): stateObj is HassEntity => Boolean(stateObj));
|
||||
|
||||
return entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(
|
||||
entitiesReg,
|
||||
config.config,
|
||||
connection.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
private _itemDisplayChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DisplayValue;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -14,8 +13,6 @@ import "./ha-list";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -243,7 +243,6 @@ export class HaNavigationPicker extends LitElement {
|
||||
items = multiTermSortedSearch(
|
||||
items,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -492,7 +492,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -8,9 +9,11 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
// WebAssembly port of ZXing:
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { configContext } from "../data/context";
|
||||
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
@@ -33,7 +36,13 @@ prepareZXingModule({
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -106,7 +115,7 @@ class HaQrScanner extends LitElement {
|
||||
${this._error || this._warning}
|
||||
${this._error
|
||||
? html`<ha-button @click=${this._retry} slot="action">
|
||||
${this.hass.localize("ui.components.qr-scanner.retry")}
|
||||
${this._localize("ui.components.qr-scanner.retry")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
@@ -126,7 +135,7 @@ class HaQrScanner extends LitElement {
|
||||
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.components.qr-scanner.select_camera"
|
||||
)}
|
||||
.path=${mdiCamera}
|
||||
@@ -146,28 +155,24 @@ class HaQrScanner extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.hass.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.hass.localize("ui.components.qr-scanner.not_supported")}
|
||||
? this._localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this._localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<p>${this._localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
.label=${this._localize("ui.components.qr-scanner.enter_qr_code")}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-input>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
${this._localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
private get _nativeBarcodeScanner(): boolean {
|
||||
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
|
||||
return Boolean(this._config.auth.external?.config.hasBarCodeScanner);
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
@@ -182,7 +187,7 @@ class HaQrScanner extends LitElement {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._reportError(
|
||||
this.hass.localize("ui.components.qr-scanner.no_camera_found")
|
||||
this._localize("ui.components.qr-scanner.no_camera_found")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -270,7 +275,7 @@ class HaQrScanner extends LitElement {
|
||||
if (msg.command === "bar_code/scan_result") {
|
||||
if (msg.payload.format !== "qr_code") {
|
||||
this._notifyExternalScanner(
|
||||
this.hass.localize("ui.components.qr-scanner.wrong_code", {
|
||||
this._localize("ui.components.qr-scanner.wrong_code", {
|
||||
format: msg.payload.format,
|
||||
rawValue: msg.payload.rawValue,
|
||||
})
|
||||
@@ -288,20 +293,17 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/scan",
|
||||
payload: {
|
||||
title:
|
||||
this.title ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.title"),
|
||||
this.title || this._localize("ui.components.qr-scanner.app.title"),
|
||||
description:
|
||||
this.description ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.description"),
|
||||
this._localize("ui.components.qr-scanner.app.description"),
|
||||
alternative_option_label:
|
||||
this.alternativeOptionLabel ||
|
||||
this.hass.localize(
|
||||
"ui.components.qr-scanner.app.alternativeOptionLabel"
|
||||
),
|
||||
this._localize("ui.components.qr-scanner.app.alternativeOptionLabel"),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -309,7 +311,7 @@ class HaQrScanner extends LitElement {
|
||||
private _closeExternalScanner() {
|
||||
this._removeListener?.();
|
||||
this._removeListener = undefined;
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/close",
|
||||
});
|
||||
}
|
||||
@@ -318,7 +320,7 @@ class HaQrScanner extends LitElement {
|
||||
if (!this._nativeBarcodeScanner) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "bar_code/notify",
|
||||
payload: {
|
||||
message,
|
||||
|
||||
@@ -23,7 +23,6 @@ export class HaAreasDisplaySelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-areas-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorTempSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-labeled-slider";
|
||||
import { generateColorTemperatureGradient } from "../../dialogs/more-info/components/lights/light-color-temp-picker";
|
||||
import {
|
||||
@@ -15,8 +14,6 @@ import {
|
||||
|
||||
@customElement("ha-selector-color_temp")
|
||||
export class HaColorTempSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorTempSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@@ -63,7 +63,6 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
if (!this.selector.entity?.multiple) {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${typeof this.value === "string" ? this.value : ""}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
@@ -80,7 +79,6 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
|
||||
@@ -84,7 +84,6 @@ export class HaLocationSelector extends LitElement {
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
|
||||
@@ -91,7 +91,6 @@ export class HaMediaSelector extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${entityId}
|
||||
.label=${this.label ||
|
||||
this.hass.localize(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiClose, mdiConnection, mdiMemory, mdiPencil, mdiUsb } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@@ -405,11 +406,12 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
let groupItems: SerialPickerItem[] = grouped[type];
|
||||
if (searchString) {
|
||||
const fuseIndex = Fuse.createIndex(DEFAULT_SEARCH_KEYS, groupItems);
|
||||
groupItems = multiTermSortedSearch(
|
||||
groupItems,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
}
|
||||
if (!groupItems.length) {
|
||||
|
||||
@@ -3,15 +3,13 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import "../ha-textarea";
|
||||
import "../input/ha-input";
|
||||
import "../input/ha-input-multi";
|
||||
|
||||
@customElement("ha-selector-text")
|
||||
export class HaTextSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@@ -558,7 +558,6 @@ export class HaServiceControl extends LitElement {
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -1116,7 +1116,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Schema } from "js-yaml";
|
||||
import { DEFAULT_SCHEMA, dump, load } from "js-yaml";
|
||||
import { dump, load, YAML11_SCHEMA } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -30,7 +30,7 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
export class HaYamlEditor extends LitElement {
|
||||
@property() public value?: any;
|
||||
|
||||
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
|
||||
@property({ attribute: false }) public yamlSchema: Schema = YAML11_SCHEMA;
|
||||
|
||||
@property({ attribute: false }) public defaultValue?: any;
|
||||
|
||||
@@ -70,7 +70,6 @@ export class HaYamlEditor extends LitElement {
|
||||
this._yaml = !isEmpty(value)
|
||||
? dump(value, {
|
||||
schema: this.yamlSchema,
|
||||
quotingType: '"',
|
||||
noRefs: true,
|
||||
})
|
||||
: "";
|
||||
|
||||
@@ -41,7 +41,11 @@ export class HaInputSearch extends HaInput {
|
||||
...HaInput.styles,
|
||||
css`
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 40px;
|
||||
height: var(--ha-input-search-height, 40px);
|
||||
border-radius: var(
|
||||
--ha-input-search-border-radius,
|
||||
var(--ha-border-radius-md)
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -13,7 +13,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import type { ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
@@ -45,8 +45,6 @@ export interface MarkerLocation {
|
||||
|
||||
@customElement("ha-locations-editor")
|
||||
export class HaLocationsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public locations?: MarkerLocation[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@@ -100,7 +100,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
: nothing}
|
||||
<div class="content">
|
||||
<ha-media-player-toggle
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
checked
|
||||
disabled
|
||||
@@ -108,7 +107,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
${this._mediaPlayerEntities(this.hass.entities).map(
|
||||
(entity) =>
|
||||
html`<ha-media-player-toggle
|
||||
.hass=${this.hass}
|
||||
.entityId=${entity.entity_id}
|
||||
.checked=${this._selectedEntities.includes(entity.entity_id)}
|
||||
@change=${this._handleSelectedChange}
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../../data/context";
|
||||
|
||||
import "../ha-switch";
|
||||
import "../ha-svg-icon";
|
||||
|
||||
@customElement("ha-media-player-toggle")
|
||||
class HaMediaPlayerToggle extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@property({ type: Boolean }) public checked = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
@state()
|
||||
private _entities!: ContextType<typeof entitiesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
@state()
|
||||
private _devices!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private _computeDisplayData = memoizeOne(
|
||||
(
|
||||
entityId: string,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
entities: ContextType<typeof entitiesContext>,
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
isRTL: boolean,
|
||||
stateObj: HomeAssistant["states"][string]
|
||||
stateObj: HassEntity
|
||||
) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
@@ -50,7 +81,11 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let icon = mdiSpeaker;
|
||||
if (stateObj.state === "playing") {
|
||||
@@ -60,16 +95,16 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors,
|
||||
isRTL,
|
||||
stateObj
|
||||
);
|
||||
|
||||
@@ -7,8 +7,8 @@ import { customElement, property } from "lit/decorators";
|
||||
export class HaProgressRing extends ProgressRing {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
public updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (changedProps.has("size")) {
|
||||
switch (this.size) {
|
||||
|
||||
@@ -44,7 +44,6 @@ import type {
|
||||
IfActionTraceStep,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-service-icon";
|
||||
import "./hat-graph-branch";
|
||||
@@ -76,8 +75,6 @@ export class HatScriptGraph extends LitElement {
|
||||
@query("hat-graph-node[active], hat-graph-branch[active]")
|
||||
private _activeNode?: HTMLElement;
|
||||
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
public renderedNodes: Record<string, NodeInfo> = {};
|
||||
|
||||
public trackedNodes: Record<string, NodeInfo> = {};
|
||||
@@ -457,7 +454,6 @@ export class HatScriptGraph extends LitElement {
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AutomationElementGroupCollection } from "./automation";
|
||||
import type { Selector, TargetSelector } from "./selector";
|
||||
|
||||
@@ -68,10 +68,10 @@ export interface ConditionDescription {
|
||||
export type ConditionDescriptions = Record<string, ConditionDescription>;
|
||||
|
||||
export const subscribeConditions = (
|
||||
hass: HomeAssistant,
|
||||
connection: Connection,
|
||||
callback: (conditions: ConditionDescriptions) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ConditionDescriptions>(callback, {
|
||||
connection.subscribeMessage<ConditionDescriptions>(callback, {
|
||||
type: "condition_platforms/subscribe",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -12,11 +12,13 @@ import type {
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import type { ConditionDescriptions } from "../condition";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../integration";
|
||||
import type { LabelRegistryEntry } from "../label/label_registry";
|
||||
import type { ItemType } from "../search";
|
||||
import type { TriggerDescriptions } from "../trigger";
|
||||
|
||||
/**
|
||||
* Entity, device, area, and floor registries
|
||||
@@ -131,6 +133,19 @@ export const configEntriesContext =
|
||||
export const manifestsContext =
|
||||
createContext<DomainManifestLookup>("manifests");
|
||||
|
||||
/**
|
||||
* Lazy loaded trigger platform descriptions, keyed by trigger key.
|
||||
*/
|
||||
export const triggerDescriptionsContext = createContext<TriggerDescriptions>(
|
||||
"triggerDescriptions"
|
||||
);
|
||||
|
||||
/**
|
||||
* Lazy loaded condition platform descriptions, keyed by condition key.
|
||||
*/
|
||||
export const conditionDescriptionsContext =
|
||||
createContext<ConditionDescriptions>("conditionDescriptions");
|
||||
|
||||
// #endregion lazy-contexts
|
||||
|
||||
// #region deprecated-contexts
|
||||
@@ -196,4 +211,33 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param context - The context to set, or undefined to clear.
|
||||
*/
|
||||
export const fireRelatedContext = (
|
||||
node: HTMLElement,
|
||||
context: RelatedContextItem | undefined
|
||||
): void => {
|
||||
fireEvent(node, "hass-related-context", context);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the related context to an entity (or clear it when no entity), so nearby
|
||||
* pickers float relevant entities. Fired by editors.
|
||||
* @param node - The node to fire the event on.
|
||||
* @param entityId - The entity to set, or undefined to clear.
|
||||
*/
|
||||
export const fireEntityRelatedContext = (
|
||||
node: HTMLElement,
|
||||
entityId: string | undefined
|
||||
): void => {
|
||||
fireRelatedContext(
|
||||
node,
|
||||
entityId ? { itemType: "entity", itemId: entityId } : undefined
|
||||
);
|
||||
};
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
+42
-7
@@ -42,6 +42,12 @@ import {
|
||||
|
||||
export const ENERGY_COLLECTION_KEY_PREFIX = "energy_";
|
||||
|
||||
// Collection key for the statistics-based energy dashboard views (Overview,
|
||||
// Electricity, Gas, Water).
|
||||
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
|
||||
// Collection key for the real-time "Now" view (live power + 5-minute stats).
|
||||
export const DEFAULT_POWER_COLLECTION_KEY = "energy_dashboard_now";
|
||||
|
||||
// All collection keys created this session
|
||||
const energyCollectionKeys = new Set<string | undefined>();
|
||||
|
||||
@@ -787,9 +793,30 @@ const findEnergyDataCollection = (
|
||||
return (hass.connection as any)[key];
|
||||
};
|
||||
|
||||
// When does the collection's day period need to roll over to the next day?
|
||||
// With `midnightRollover` (the real-time "Now" view) it rolls over right at
|
||||
// midnight. Otherwise it waits an hour, until the new day's first hourly
|
||||
// statistic exists — rolling over at midnight would show an empty graph.
|
||||
export const getNextEnergyPeriodStart = (
|
||||
midnightRollover: boolean,
|
||||
now: Date,
|
||||
locale: HomeAssistant["locale"],
|
||||
config: HomeAssistant["config"]
|
||||
): Date => {
|
||||
const dayEnd = calcDate(now, endOfDay, locale, config);
|
||||
return midnightRollover ? addMilliseconds(dayEnd, 1) : addHours(dayEnd, 1);
|
||||
};
|
||||
|
||||
export const getEnergyDataCollection = (
|
||||
hass: HomeAssistant,
|
||||
options: { prefs?: EnergyPreferences; key?: string } = {}
|
||||
options: {
|
||||
prefs?: EnergyPreferences;
|
||||
key?: string;
|
||||
// The real-time "Now" view opts in to rolling its day period over at
|
||||
// midnight rather than an hour later (it shows live data, so it always
|
||||
// tracks today and never falls back to yesterday in the first hour).
|
||||
midnightRollover?: boolean;
|
||||
} = {}
|
||||
): EnergyCollection => {
|
||||
const [key, collectionKey] = convertCollectionKeyToConnection(
|
||||
hass,
|
||||
@@ -799,6 +826,8 @@ export const getEnergyDataCollection = (
|
||||
return (hass.connection as any)[key];
|
||||
}
|
||||
|
||||
const midnightRollover = options.midnightRollover ?? false;
|
||||
|
||||
energyCollectionKeys.add(collectionKey);
|
||||
|
||||
const collection = getCollection<EnergyData>(
|
||||
@@ -857,12 +886,16 @@ export const getEnergyDataCollection = (
|
||||
|
||||
const now = new Date();
|
||||
const hour = formatTime24h(now, hass.locale, hass.config).split(":")[0];
|
||||
// Set start to start of today if we have data for today, otherwise yesterday
|
||||
// Set start to start of today if we have data for today, otherwise yesterday.
|
||||
// The real-time "Now" view always tracks today; it shows live data even
|
||||
// before today's first statistic exists, so it never falls back to yesterday.
|
||||
const preferredPeriod =
|
||||
(localStorage.getItem(`energy-default-period-${key}`) as DateRange) ||
|
||||
"today";
|
||||
const period =
|
||||
preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod;
|
||||
preferredPeriod === "today" && hour === "0" && !midnightRollover
|
||||
? "yesterday"
|
||||
: preferredPeriod;
|
||||
|
||||
const [start, end] = calcDateRange(hass.locale, hass.config, period);
|
||||
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
|
||||
@@ -886,10 +919,12 @@ export const getEnergyDataCollection = (
|
||||
collection.refresh();
|
||||
scheduleUpdatePeriod();
|
||||
},
|
||||
addHours(
|
||||
calcDate(new Date(), endOfDay, hass.locale, hass.config),
|
||||
1
|
||||
).getTime() - Date.now() // Switch to next day an hour after the day changed
|
||||
getNextEnergyPeriodStart(
|
||||
midnightRollover,
|
||||
new Date(),
|
||||
hass.locale,
|
||||
hass.config
|
||||
).getTime() - Date.now()
|
||||
);
|
||||
};
|
||||
scheduleUpdatePeriod();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { getEntityAreaId } from "../../common/entity/context/get_entity_context";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
@@ -12,6 +15,7 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
stateObj?: HassEntity;
|
||||
relatedRank?: number;
|
||||
}
|
||||
|
||||
export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
@@ -54,7 +58,17 @@ export interface GetEntitiesOptions {
|
||||
}
|
||||
|
||||
export const getEntities = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<
|
||||
HomeAssistant,
|
||||
| "states"
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "areas"
|
||||
| "floors"
|
||||
| "language"
|
||||
| "translationMetadata"
|
||||
| "localize"
|
||||
>,
|
||||
options?: GetEntitiesOptions
|
||||
): EntityComboBoxItem[] => {
|
||||
const {
|
||||
@@ -186,3 +200,72 @@ export const getEntities = (
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const RELATED_RANK_UNRELATED = 3;
|
||||
|
||||
const entityRelatedRank = (
|
||||
entityId: string | undefined,
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): number => {
|
||||
if (!entityId) {
|
||||
return RELATED_RANK_UNRELATED;
|
||||
}
|
||||
if (related.entities.has(entityId)) {
|
||||
return 0;
|
||||
}
|
||||
const deviceId = entities[entityId]?.device_id;
|
||||
if (deviceId && related.devices.has(deviceId)) {
|
||||
return 1;
|
||||
}
|
||||
const areaId = getEntityAreaId(entityId, entities, devices);
|
||||
if (areaId && related.areas.has(areaId)) {
|
||||
return 2;
|
||||
}
|
||||
return RELATED_RANK_UNRELATED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Annotate entity items with their closeness to the related context, so they
|
||||
* can be floated to the top. The entity itself ranks closest, then its device,
|
||||
* then its area; anything unrelated keeps the lowest rank.
|
||||
*/
|
||||
export const markEntitiesRelated = (
|
||||
items: EntityComboBoxItem[],
|
||||
related: RelatedIdSets,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): EntityComboBoxItem[] =>
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
relatedRank: entityRelatedRank(
|
||||
item.stateObj?.entity_id,
|
||||
related,
|
||||
entities,
|
||||
devices
|
||||
),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Sort entity items by related closeness (entity, then device, then area, then
|
||||
* the rest). Pass `language` to break ties within a tier alphabetically by
|
||||
* label; omit it to keep the incoming order (e.g. search relevance).
|
||||
*/
|
||||
export const sortEntitiesByRelatedRank = (
|
||||
items: EntityComboBoxItem[],
|
||||
language?: string
|
||||
): EntityComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const rankDiff =
|
||||
(a.relatedRank ?? RELATED_RANK_UNRELATED) -
|
||||
(b.relatedRank ?? RELATED_RANK_UNRELATED);
|
||||
if (rankDiff !== 0 || language === undefined) {
|
||||
return rankDiff;
|
||||
}
|
||||
return caseInsensitiveStringCompare(
|
||||
a.sorting_label ?? "",
|
||||
b.sorting_label ?? "",
|
||||
language
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
|
||||
import type { LovelaceBadgeConfig } from "./lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "./lovelace/config/card";
|
||||
|
||||
export interface CustomCardSuggestion<
|
||||
@@ -10,6 +11,13 @@ export interface CustomCardSuggestion<
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomBadgeSuggestion<
|
||||
T extends LovelaceBadgeConfig = LovelaceBadgeConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
@@ -28,6 +36,10 @@ export interface CustomBadgeEntry {
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
getEntitySuggestion?: (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => CustomBadgeSuggestion | CustomBadgeSuggestion[] | null;
|
||||
}
|
||||
|
||||
export interface CustomCardFeatureEntry {
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
AutomationElementGroupCollection,
|
||||
Trigger,
|
||||
@@ -73,10 +73,10 @@ export interface TriggerDescription {
|
||||
export type TriggerDescriptions = Record<string, TriggerDescription>;
|
||||
|
||||
export const subscribeTriggers = (
|
||||
hass: HomeAssistant,
|
||||
connection: Connection,
|
||||
callback: (triggers: TriggerDescriptions) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<TriggerDescriptions>(callback, {
|
||||
connection.subscribeMessage<TriggerDescriptions>(callback, {
|
||||
type: "trigger_platforms/subscribe",
|
||||
});
|
||||
|
||||
|
||||
@@ -59,14 +59,12 @@ import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import { subscribeLabFeature } from "../../data/labs";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { DirtyStateProviderMixin } from "../../mixins/dirty-state-provider-mixin";
|
||||
import type { EntitySettingsState } from "../../panels/config/entities/entity-registry-settings-editor";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import {
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
@@ -126,7 +124,7 @@ const DEFAULT_VIEW: MoreInfoView = "info";
|
||||
export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
EntitySettingsState | Helper | Record<string, string[]> | null,
|
||||
"entity-registry" | "helper" | "vacuum-segment-mapping"
|
||||
>()(SubscribeMixin(ScrollableFadeMixin(LitElement))) {
|
||||
>()(ScrollableFadeMixin(LitElement)) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
@@ -163,8 +161,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
|
||||
@state() private _isEscapeEnabled = true;
|
||||
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
protected scrollFadeThreshold = 24;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
@@ -260,24 +256,11 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
|
||||
private _shouldShowAddEntityTo(): boolean {
|
||||
return (
|
||||
(this._newTriggersAndConditions && !!this.hass.user?.is_admin) ||
|
||||
!!this.hass.user?.is_admin ||
|
||||
!!this.hass.auth.external?.config.hasEntityAddTo
|
||||
);
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersAndConditions = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _getDeviceId(): string | null {
|
||||
const entity = this.hass.entities[this._entityId!] as
|
||||
| EntityRegistryEntry
|
||||
|
||||
@@ -42,7 +42,7 @@ export class MoreInfoLogbook extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.time=${this._time}
|
||||
.entityIds=${this._entityIdAsList(this.entityId)}
|
||||
.scope=${"entity"}
|
||||
name-detail="none"
|
||||
narrow
|
||||
no-icon
|
||||
graph-color
|
||||
|
||||
@@ -460,8 +460,7 @@ export class QuickBar extends LitElement {
|
||||
navigateItems = this._filterGroup(
|
||||
"navigate",
|
||||
navigateItems,
|
||||
filter,
|
||||
navigateComboBoxKeys
|
||||
filter
|
||||
) as NavigationComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -482,8 +481,7 @@ export class QuickBar extends LitElement {
|
||||
commandItems = this._filterGroup(
|
||||
"command",
|
||||
commandItems,
|
||||
filter,
|
||||
commandComboBoxKeys
|
||||
filter
|
||||
) as ActionCommandComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -513,8 +511,7 @@ export class QuickBar extends LitElement {
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
filter
|
||||
) as EntityComboBoxItem[]
|
||||
);
|
||||
} else {
|
||||
@@ -550,7 +547,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
if (filter) {
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
this._filterGroup("device", deviceItems, filter)
|
||||
);
|
||||
} else {
|
||||
deviceItems = this._sortRelatedByLabel(deviceItems);
|
||||
@@ -582,7 +579,7 @@ export class QuickBar extends LitElement {
|
||||
|
||||
if (filter) {
|
||||
areaItems = sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
this._filterGroup("area", areaItems, filter)
|
||||
);
|
||||
} else {
|
||||
areaItems = this._sortRelatedByLabel(areaItems);
|
||||
@@ -660,15 +657,13 @@ export class QuickBar extends LitElement {
|
||||
private _filterGroup(
|
||||
type: QuickBarSection,
|
||||
items: PickerComboBoxItem[],
|
||||
searchTerm: string,
|
||||
weightedKeys: FuseWeightedKey[]
|
||||
searchTerm: string
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item: PickerComboBoxItem) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
applyThemesOnElement,
|
||||
@@ -6,6 +7,16 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFormatFunctions } from "../common/translations/entity-state";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
uiContext,
|
||||
} from "../data/context";
|
||||
import { updateHassGroups } from "../data/context/updateContext";
|
||||
import type { IconCategory } from "../data/icons";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import {
|
||||
@@ -85,13 +96,55 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
export const provideHass = (
|
||||
elements,
|
||||
overrideData: Partial<HomeAssistant> = {},
|
||||
setHassProperty = false
|
||||
setHassProperty = false,
|
||||
// Opt-in to providing the grouped Lit contexts (config, formatters, api, …)
|
||||
// that the real app's root element provides via `contextMixin`. Needed for
|
||||
// gallery demos that render context-consuming components (e.g. the climate
|
||||
// temperature control) without the full app shell.
|
||||
provideContexts = false
|
||||
): MockHomeAssistant => {
|
||||
elements = ensureArray(elements);
|
||||
// Can happen because we store sidebar, more info etc on hass.
|
||||
const baseEl = () => elements[0];
|
||||
const hass = (): MockHomeAssistant => baseEl().hass;
|
||||
|
||||
const contextProviders = provideContexts
|
||||
? {
|
||||
registries: new ContextProvider(baseEl(), {
|
||||
context: registriesContext,
|
||||
}),
|
||||
internationalization: new ContextProvider(baseEl(), {
|
||||
context: internationalizationContext,
|
||||
}),
|
||||
api: new ContextProvider(baseEl(), { context: apiContext }),
|
||||
connection: new ContextProvider(baseEl(), {
|
||||
context: connectionContext,
|
||||
}),
|
||||
ui: new ContextProvider(baseEl(), { context: uiContext }),
|
||||
config: new ContextProvider(baseEl(), { context: configContext }),
|
||||
formatters: new ContextProvider(baseEl(), {
|
||||
context: formattersContext,
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const updateContextProviders = (newHass: HomeAssistant) => {
|
||||
if (!contextProviders) {
|
||||
return;
|
||||
}
|
||||
(
|
||||
Object.keys(contextProviders) as (keyof typeof contextProviders)[]
|
||||
).forEach((group) => {
|
||||
const provider = contextProviders[group];
|
||||
provider.setValue(
|
||||
(updateHassGroups[group] as (h: HomeAssistant, v?: any) => any)(
|
||||
newHass,
|
||||
provider.value
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const wsCommands = {};
|
||||
const restResponses: [string | RegExp, MockRestCallback][] = [];
|
||||
|
||||
@@ -396,6 +449,7 @@ export const provideHass = (
|
||||
elements.forEach((el) => {
|
||||
el.hass = newHass;
|
||||
});
|
||||
updateContextProviders(newHass);
|
||||
},
|
||||
updateStates,
|
||||
updateTranslations,
|
||||
@@ -457,6 +511,10 @@ export const provideHass = (
|
||||
value: value !== null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
},
|
||||
],
|
||||
formatEntityName: (stateObj, type) =>
|
||||
typeof type === "string"
|
||||
? type
|
||||
: (stateObj.attributes.friendly_name ?? stateObj.entity_id),
|
||||
...overrideData,
|
||||
};
|
||||
|
||||
|
||||
@@ -793,6 +793,12 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
}
|
||||
@media (min-width: 871px) {
|
||||
ha-input-search {
|
||||
--ha-input-search-height: 32px;
|
||||
--ha-input-search-border-radius: 10px;
|
||||
}
|
||||
}
|
||||
.search-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -176,7 +176,6 @@ class OnboardingLocation extends LitElement {
|
||||
</div>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocations(
|
||||
this._location,
|
||||
this._places,
|
||||
|
||||
@@ -228,7 +228,6 @@ class DialogCalendarEventEditor extends DirtyStateProviderMixin<CalendarEventFor
|
||||
></ha-textarea>
|
||||
<ha-entity-picker
|
||||
name="calendar"
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.calendar.label")}
|
||||
.value=${this._calendarId!}
|
||||
.includeDomains=${CALENDAR_DOMAINS}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { DEFAULT_SCHEMA, Type } from "js-yaml";
|
||||
import { defineScalarTag, YAML11_SCHEMA } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -47,12 +47,11 @@ const SUPPORTED_UI_TYPES = [
|
||||
"schema",
|
||||
];
|
||||
|
||||
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
|
||||
new Type("!secret", {
|
||||
kind: "scalar",
|
||||
construct: (data) => `!secret ${data}`,
|
||||
}),
|
||||
]);
|
||||
const secretTag = defineScalarTag("!secret", {
|
||||
resolve: (data) => `!secret ${data}`,
|
||||
});
|
||||
|
||||
const ADDON_YAML_SCHEMA = YAML11_SCHEMA.withTags(secretTag);
|
||||
|
||||
const MASKED_FIELDS = ["password", "secret", "token"];
|
||||
|
||||
|
||||
@@ -251,7 +251,6 @@ class DialogAreaDetail
|
||||
>
|
||||
<div class="content">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.temperature_entity"
|
||||
)}
|
||||
@@ -266,7 +265,6 @@ class DialogAreaDetail
|
||||
></ha-entity-picker>
|
||||
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.humidity_entity"
|
||||
)}
|
||||
|
||||
@@ -14,20 +14,14 @@ import {
|
||||
mdiShape,
|
||||
mdiTools,
|
||||
} from "@mdi/js";
|
||||
import type {
|
||||
HassEntity,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket/dist/types";
|
||||
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -54,7 +48,7 @@ import {
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { sortDeviceRegistryByName } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
@@ -62,7 +56,6 @@ import {
|
||||
computeEntityRegistryName,
|
||||
sortEntityRegistryByName,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { subscribeLabFeature } from "../../../data/labs";
|
||||
import type { SceneEntity } from "../../../data/scene";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
@@ -72,7 +65,6 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
@@ -146,7 +138,7 @@ const NAVIGATION_ACTIONS: {
|
||||
const MAX_COLUMNS = 3;
|
||||
|
||||
@customElement("ha-config-area-page")
|
||||
class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
class HaConfigAreaPage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public areaId!: string;
|
||||
@@ -161,8 +153,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _related?: RelatedResult;
|
||||
|
||||
@state() private _newTriggersConditions = false;
|
||||
|
||||
private _logbookTime = { recent: 86400 };
|
||||
|
||||
private _columnsController = createColumnsController(this, MAX_COLUMNS);
|
||||
@@ -251,30 +241,13 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("areaId")) {
|
||||
this._findRelated();
|
||||
fireEvent(this, "hass-related-context", {
|
||||
fireRelatedContext(this, {
|
||||
itemType: "area",
|
||||
itemId: this.areaId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
if (!isComponentLoaded(this.hass!.config, "automation")) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersConditions = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass.areas || !this.hass.devices || !this.hass.entities) {
|
||||
return nothing;
|
||||
@@ -380,32 +353,26 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
></ha-icon-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
${area.picture && !this._newTriggersConditions
|
||||
? nothing
|
||||
: html`<div class="action-buttons">
|
||||
${area.picture
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.add_picture")}
|
||||
</ha-button>`}
|
||||
${this._newTriggersConditions
|
||||
? html`<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</div>`}
|
||||
<div class="action-buttons">
|
||||
${area.picture
|
||||
? nothing
|
||||
: html`<ha-button
|
||||
appearance="filled"
|
||||
.entry=${area}
|
||||
@click=${this._showSettings}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiImagePlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.areas.add_picture")}
|
||||
</ha-button>`}
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
variant="brand"
|
||||
@click=${this._showAddToDialog}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.add_to.item")}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
@@ -622,7 +589,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._allEntities(memberships)}
|
||||
.deviceIds=${this._allDeviceIds(memberships.devices)}
|
||||
.scope=${"area"}
|
||||
name-detail="device"
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -21,10 +22,9 @@ import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../../../data/condition";
|
||||
import { conditionDescriptionsContext } from "../../../../../data/context";
|
||||
import { domainToName } from "../../../../../data/integration";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
@@ -42,10 +42,7 @@ import "../../condition/types/ha-automation-condition-zone";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements ActionElement
|
||||
{
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -58,7 +55,9 @@ export class HaConditionAction
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query("ha-automation-condition-editor")
|
||||
private _conditionEditor?: HaAutomationConditionEditor;
|
||||
@@ -67,21 +66,6 @@ export class HaConditionAction
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
mdiHelpCircleOutline,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import type {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -80,13 +77,16 @@ import {
|
||||
CONDITION_COLLECTIONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../data/condition";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
import { labelsContext } from "../../../data/context";
|
||||
import {
|
||||
conditionDescriptionsContext,
|
||||
labelsContext,
|
||||
triggerDescriptionsContext,
|
||||
} from "../../../data/context";
|
||||
import { getDeviceEntityLookup } from "../../../data/device/device_registry";
|
||||
import type { EntityComboBoxItem } from "../../../data/entity/entity_picker";
|
||||
import { getFloorAreaLookup } from "../../../data/floor_registry";
|
||||
@@ -101,7 +101,6 @@ import {
|
||||
fetchIntegrationManifests,
|
||||
} from "../../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
|
||||
import { subscribeLabFeature } from "../../../data/labs";
|
||||
import { filterSelectorEntities } from "../../../data/selector";
|
||||
import {
|
||||
TARGET_SEPARATOR,
|
||||
@@ -116,7 +115,6 @@ import {
|
||||
TRIGGER_COLLECTIONS,
|
||||
getTriggerDomain,
|
||||
getTriggerObjectId,
|
||||
subscribeTriggers,
|
||||
} from "../../../data/trigger";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
@@ -227,7 +225,13 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
@state()
|
||||
@consume({ context: triggerDescriptionsContext, subscribe: true })
|
||||
private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state() private _targetItems?: {
|
||||
title: string;
|
||||
@@ -236,12 +240,8 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _loadItemsError = false;
|
||||
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
@state() private _openedFromQuery = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labelRegistry!: LabelRegistryEntry[];
|
||||
@@ -259,10 +259,6 @@ class DialogAddAutomationElement
|
||||
|
||||
// #region variables
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _unsubscribeLabFeatures?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
|
||||
private _closing = false;
|
||||
@@ -278,31 +274,6 @@ class DialogAddAutomationElement
|
||||
) {
|
||||
this._calculateUsedDomains();
|
||||
}
|
||||
|
||||
if (changedProps.has("_newTriggersAndConditions")) {
|
||||
this._subscribeDescriptions();
|
||||
}
|
||||
}
|
||||
|
||||
private _subscribeDescriptions() {
|
||||
this._unsubscribe();
|
||||
if (this._params?.type === "trigger") {
|
||||
this._triggerDescriptions = {};
|
||||
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
||||
this._triggerDescriptions = {
|
||||
...this._triggerDescriptions,
|
||||
...triggers,
|
||||
};
|
||||
});
|
||||
} else if (this._params?.type === "condition") {
|
||||
this._conditionDescriptions = {};
|
||||
this._unsub = subscribeConditions(this.hass, (conditions) => {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public showDialog(params: AddAutomationElementDialogParams): void {
|
||||
@@ -334,28 +305,9 @@ class DialogAddAutomationElement
|
||||
|
||||
this._loadConfigEntries();
|
||||
|
||||
this._unsubscribe();
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
|
||||
this._unsubscribeLabFeatures = subscribeLabFeature(
|
||||
this.hass.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersAndConditions = feature.enabled;
|
||||
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
|
||||
if (
|
||||
queryTarget &&
|
||||
this._newTriggersAndConditions &&
|
||||
!this._selectedTarget
|
||||
) {
|
||||
this._selectedTarget = queryTarget;
|
||||
this._getItemsByTarget();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!queryTarget) {
|
||||
// add initial dialog view state to history
|
||||
mainWindow.history.pushState(
|
||||
@@ -372,11 +324,9 @@ class DialogAddAutomationElement
|
||||
} else if (this._params?.type === "trigger") {
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
getTriggerIcons(this.hass.connection, this.hass.config);
|
||||
this._subscribeDescriptions();
|
||||
} else if (this._params?.type === "condition") {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
getConditionIcons(this.hass.connection, this.hass.config);
|
||||
this._subscribeDescriptions();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this._updateNarrow);
|
||||
@@ -385,11 +335,7 @@ class DialogAddAutomationElement
|
||||
// prevent view mode switch when resizing window
|
||||
this._bottomSheetMode = this._narrow;
|
||||
|
||||
if (
|
||||
queryTarget &&
|
||||
this._newTriggersAndConditions &&
|
||||
!this._selectedTarget
|
||||
) {
|
||||
if (queryTarget && !this._selectedTarget) {
|
||||
this._selectedTarget = queryTarget;
|
||||
this._tab = "targets";
|
||||
this._getItemsByTarget();
|
||||
@@ -434,7 +380,6 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
this.removeKeyboardShortcuts();
|
||||
this._unsubscribe();
|
||||
if (this._params) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -450,7 +395,7 @@ class DialogAddAutomationElement
|
||||
this._selectedCollectionIndex = undefined;
|
||||
this._selectedGroup = undefined;
|
||||
this._selectedTarget = undefined;
|
||||
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
|
||||
this._tab = "targets";
|
||||
this._filter = "";
|
||||
this._manifests = undefined;
|
||||
this._domains = undefined;
|
||||
@@ -589,7 +534,6 @@ class DialogAddAutomationElement
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._updateNarrow);
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
@@ -598,39 +542,10 @@ class DialogAddAutomationElement
|
||||
};
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this._unsubscribeLabFeatures) {
|
||||
this._unsubscribeLabFeatures.then((unsub) => unsub());
|
||||
this._unsubscribeLabFeatures = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion lifecycle
|
||||
|
||||
// #region render
|
||||
|
||||
private _getEmptyNote(automationElementType: string) {
|
||||
if (
|
||||
automationElementType !== "trigger" &&
|
||||
automationElementType !== "condition"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target_note`,
|
||||
{
|
||||
labs_link: html`<a href="/config/labs" @click=${this._close}
|
||||
>${this.hass.localize("ui.panel.config.labs.caption")}</a
|
||||
>`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -664,6 +579,12 @@ class DialogAddAutomationElement
|
||||
const automationElementType = this._params!.type;
|
||||
|
||||
const tabButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.tabs.target"
|
||||
),
|
||||
value: "targets",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.tabs.type"
|
||||
@@ -672,15 +593,6 @@ class DialogAddAutomationElement
|
||||
},
|
||||
];
|
||||
|
||||
if (this._newTriggersAndConditions) {
|
||||
tabButtons.unshift({
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.tabs.target"
|
||||
),
|
||||
value: "targets",
|
||||
});
|
||||
}
|
||||
|
||||
if (this._params?.type !== "trigger") {
|
||||
tabButtons.push({
|
||||
label: this.hass.localize("ui.panel.config.automation.editor.blocks"),
|
||||
@@ -763,7 +675,6 @@ class DialogAddAutomationElement
|
||||
this._manifests
|
||||
)}
|
||||
.convertToItem=${this._convertToItem}
|
||||
.newTriggersAndConditions=${this._newTriggersAndConditions}
|
||||
@search-element-picked=${this._searchItemSelected}
|
||||
>
|
||||
</ha-automation-add-search>`
|
||||
@@ -889,7 +800,6 @@ class DialogAddAutomationElement
|
||||
.emptyLabel=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
|
||||
)}
|
||||
.emptyNote=${this._getEmptyNote(automationElementType)}
|
||||
.tooltipDescription=${this._tab === "targets"}
|
||||
.target=${(this._tab === "targets" &&
|
||||
this._selectedTarget &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -40,8 +40,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
|
||||
@property({ attribute: "empty-label" }) public emptyLabel!: string;
|
||||
|
||||
@property({ attribute: false }) public emptyNote?: string | TemplateResult;
|
||||
|
||||
@property({ attribute: false }) public target?: Target;
|
||||
|
||||
@property({ attribute: false }) public getLabel!: (
|
||||
@@ -83,9 +81,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
? html`${this.emptyLabel}
|
||||
${this.target
|
||||
? html`<div>${this._renderTarget(this.target)}</div>`
|
||||
: nothing}
|
||||
${this.emptyNote
|
||||
? html`<div class="empty-note">${this.emptyNote}</div>`
|
||||
: nothing}`
|
||||
: repeat(
|
||||
this.items,
|
||||
@@ -232,17 +227,6 @@ export class HaAutomationAddItems extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-note {
|
||||
color: var(--ha-color-text-secondary);
|
||||
margin-top: var(--ha-space-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-note a {
|
||||
color: currentColor;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.items.error {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
color: var(--ha-color-on-danger-normal);
|
||||
|
||||
@@ -117,9 +117,6 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "new-triggers-and-conditions" })
|
||||
public newTriggersAndConditions = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public convertToItem!: (
|
||||
key: string,
|
||||
@@ -209,7 +206,6 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
this.filter,
|
||||
this.configEntryLookup,
|
||||
this.items,
|
||||
this.newTriggersAndConditions,
|
||||
this._selectedSearchSection,
|
||||
this._relatedIdSets
|
||||
);
|
||||
@@ -260,19 +256,13 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
}
|
||||
|
||||
private _renderSections() {
|
||||
if (this.addElementType === "trigger" && !this.newTriggersAndConditions) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const searchSections: ("separator" | SearchSection)[] = ["item"];
|
||||
|
||||
if (this.addElementType !== "trigger") {
|
||||
searchSections.push("block");
|
||||
}
|
||||
|
||||
if (this.newTriggersAndConditions) {
|
||||
searchSections.push(...TARGET_SEARCH_SECTIONS);
|
||||
}
|
||||
searchSections.push(...TARGET_SEARCH_SECTIONS);
|
||||
return html`
|
||||
<ha-chip-set class="sections">
|
||||
${searchSections.map((section) =>
|
||||
@@ -502,7 +492,6 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
searchTerm: string,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
automationItems: AddAutomationElementListItem[],
|
||||
newTriggersAndConditions: boolean,
|
||||
selectedSection?: SearchSection,
|
||||
relatedIdSets?: RelatedIdSets
|
||||
) => {
|
||||
@@ -570,191 +559,185 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
resultItems.push(...blocks);
|
||||
}
|
||||
|
||||
if (newTriggersAndConditions) {
|
||||
if (!selectedSection || selectedSection === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
`entity${TARGET_SEPARATOR}`
|
||||
);
|
||||
if (!selectedSection || selectedSection === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
`entity${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
})) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
)
|
||||
) as EntityComboBoxItem[];
|
||||
} else if (relatedIdSets?.entities.size) {
|
||||
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && entityItems.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.entities")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(...entityItems);
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "device") {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
`device${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
)
|
||||
);
|
||||
} else if (relatedIdSets?.devices.size) {
|
||||
deviceItems = sortRelatedFirst(deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.devices")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(...deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "area") {
|
||||
let areasAndFloors = this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(TARGET_SEPARATOR)
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = areasAndFloors.map((item) => ({
|
||||
...item,
|
||||
isRelated:
|
||||
item.type === "area"
|
||||
? relatedIdSets.areas.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
)
|
||||
: false,
|
||||
})) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
)
|
||||
) as FloorComboBoxItem[];
|
||||
} else if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
areasAndFloors
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && areasAndFloors.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.areas")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(
|
||||
...areasAndFloors.map((item, index) => {
|
||||
const nextItem = areasAndFloors[index + 1];
|
||||
|
||||
if (
|
||||
!nextItem ||
|
||||
(item.type === "area" && nextItem.type === "floor")
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
})) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`label${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup(
|
||||
"label",
|
||||
labels,
|
||||
if (searchTerm) {
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
labelComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSection && labels.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.labels")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(...labels);
|
||||
entityComboBoxKeys
|
||||
)
|
||||
) as EntityComboBoxItem[];
|
||||
} else if (relatedIdSets?.entities.size) {
|
||||
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && entityItems.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.entities")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(...entityItems);
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "device") {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
`device${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
)
|
||||
);
|
||||
} else if (relatedIdSets?.devices.size) {
|
||||
deviceItems = sortRelatedFirst(deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
// show group title
|
||||
resultItems.push(
|
||||
localize("ui.components.target-picker.type.devices")
|
||||
);
|
||||
}
|
||||
|
||||
resultItems.push(...deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "area") {
|
||||
let areasAndFloors = this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(TARGET_SEPARATOR)
|
||||
),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = areasAndFloors.map((item) => ({
|
||||
...item,
|
||||
isRelated:
|
||||
item.type === "area"
|
||||
? relatedIdSets.areas.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
)
|
||||
: false,
|
||||
})) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
)
|
||||
) as FloorComboBoxItem[];
|
||||
} else if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
areasAndFloors
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && areasAndFloors.length) {
|
||||
// show group title
|
||||
resultItems.push(localize("ui.components.target-picker.type.areas"));
|
||||
}
|
||||
|
||||
resultItems.push(
|
||||
...areasAndFloors.map((item, index) => {
|
||||
const nextItem = areasAndFloors[index + 1];
|
||||
|
||||
if (
|
||||
!nextItem ||
|
||||
(item.type === "area" && nextItem.type === "floor")
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
last: true,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSection || selectedSection === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`label${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup(
|
||||
"label",
|
||||
labels,
|
||||
searchTerm,
|
||||
labelComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedSection && labels.length) {
|
||||
// show group title
|
||||
resultItems.push(localize("ui.components.target-picker.type.labels"));
|
||||
}
|
||||
|
||||
resultItems.push(...labels);
|
||||
}
|
||||
|
||||
return resultItems;
|
||||
@@ -779,7 +762,6 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
return multiTermSortedSearch<PickerComboBoxItem>(
|
||||
items,
|
||||
searchTerm,
|
||||
searchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
@@ -19,12 +17,8 @@ import {
|
||||
type Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
subscribeConditions,
|
||||
} from "../../../../data/condition";
|
||||
import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { conditionDescriptionsContext } from "../../../../data/context";
|
||||
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
|
||||
import {
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
@@ -38,7 +32,7 @@ import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends AutomationSortableListMixin<Condition>(
|
||||
SubscribeMixin(LitElement)
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public conditions!: Condition[];
|
||||
|
||||
@@ -48,16 +42,13 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public editorDirty = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@queryAll("ha-automation-condition-row")
|
||||
private _conditionRowElements?: HaAutomationConditionRow[];
|
||||
|
||||
// @ts-ignore
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _openedAddDialogFromQuery = false;
|
||||
|
||||
protected get items(): Condition[] {
|
||||
@@ -72,49 +63,6 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
|
||||
this.highlightedConditions = items;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersAndConditions = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _subscribeDescriptions() {
|
||||
this._unsubscribe();
|
||||
this._conditionDescriptions = {};
|
||||
this._unsub = subscribeConditions(this.hass, (descriptions) => {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...descriptions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("_newTriggersAndConditions")) {
|
||||
this._subscribeDescriptions();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
|
||||
@@ -38,7 +38,6 @@ export class HaZoneCondition extends LitElement {
|
||||
)}
|
||||
.value=${entity_id}
|
||||
@value-changed=${this._entityPicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.entityFilter=${zoneAndLocationFilter}
|
||||
></ha-entity-picker>
|
||||
@@ -48,7 +47,6 @@ export class HaZoneCondition extends LitElement {
|
||||
)}
|
||||
.value=${zone}
|
||||
@value-changed=${this._zonePicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.includeDomains=${includeDomains}
|
||||
></ha-entity-picker>
|
||||
|
||||
@@ -8,12 +8,11 @@ import type {
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -167,9 +166,8 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
}
|
||||
|
||||
this._relatedContextAreaId = areaId;
|
||||
fireEvent(
|
||||
fireRelatedContext(
|
||||
this,
|
||||
"hass-related-context",
|
||||
areaId
|
||||
? {
|
||||
itemType: "area",
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
NodeInfo,
|
||||
} from "../../../components/trace/hat-script-graph";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import type { LogbookEntry } from "../../../data/logbook";
|
||||
import { getLogbookDataForContext } from "../../../data/logbook";
|
||||
@@ -228,7 +228,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
<div class="main">
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
@@ -378,9 +377,8 @@ export class HaAutomationTrace extends LitElement {
|
||||
(entry) => entry.entity_id === this._entityId
|
||||
)?.area_id
|
||||
: undefined;
|
||||
fireEvent(
|
||||
fireRelatedContext(
|
||||
this,
|
||||
"hass-related-context",
|
||||
areaId
|
||||
? {
|
||||
itemType: "area",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { load } from "js-yaml";
|
||||
import { load, YAML11_SCHEMA } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
@@ -224,7 +224,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
|
||||
let loaded: any;
|
||||
try {
|
||||
loaded = load(paste);
|
||||
loaded = load(paste, { schema: YAML11_SCHEMA });
|
||||
} catch (_err: any) {
|
||||
showEditorToast(this, {
|
||||
message: this.hass.localize(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -19,10 +17,9 @@ import {
|
||||
type Trigger,
|
||||
type TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
import { subscribeLabFeature } from "../../../../data/labs";
|
||||
import { triggerDescriptionsContext } from "../../../../data/context";
|
||||
import type { TriggerDescriptions } from "../../../../data/trigger";
|
||||
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
|
||||
import {
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
@@ -36,7 +33,7 @@ import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends AutomationSortableListMixin<Trigger>(
|
||||
SubscribeMixin(LitElement)
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public triggers!: Trigger[];
|
||||
|
||||
@@ -46,12 +43,9 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public editorDirty = false;
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
// @ts-ignore
|
||||
@state() private _newTriggersAndConditions = false;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
@state()
|
||||
@consume({ context: triggerDescriptionsContext, subscribe: true })
|
||||
private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
private _openedAddDialogFromQuery = false;
|
||||
|
||||
@@ -67,49 +61,6 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
|
||||
this.highlightedTriggers = items;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersAndConditions = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _subscribeDescriptions() {
|
||||
this._unsubscribe();
|
||||
this._triggerDescriptions = {};
|
||||
this._unsub = subscribeTriggers(this.hass, (descriptions) => {
|
||||
this._triggerDescriptions = {
|
||||
...this._triggerDescriptions,
|
||||
...descriptions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("_newTriggersAndConditions")) {
|
||||
this._subscribeDescriptions();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
|
||||
@@ -45,7 +45,6 @@ export class HaZoneTrigger extends LitElement {
|
||||
.value=${entity_id ? ensureArray(entity_id) : []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._entityPicked}
|
||||
.hass=${this.hass}
|
||||
.entityFilter=${zoneAndLocationFilter}
|
||||
></ha-entities-picker>
|
||||
<ha-entity-picker
|
||||
@@ -55,7 +54,6 @@ export class HaZoneTrigger extends LitElement {
|
||||
.value=${zone}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._zonePicked}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${includeDomains}
|
||||
></ha-entity-picker>
|
||||
|
||||
|
||||
@@ -508,7 +508,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
|
||||
.value=${this._filters[TYPE_FILTER]}
|
||||
.states=${this._states(this.hass.localize, isHassio)}
|
||||
@@ -517,7 +516,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
.narrow=${this.narrow}
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.config.backup.locations")}
|
||||
.value=${this._filters[LOCATIONS_FILTER]}
|
||||
.states=${this._locations(
|
||||
|
||||
@@ -60,7 +60,7 @@ export class DialogManageCloudhook extends LitElement {
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
${!cloudhook.managed
|
||||
${cloudhook.managed
|
||||
? html`
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.cloud.dialog_cloudhook.managed_by_integration"
|
||||
|
||||
@@ -99,7 +99,6 @@ export class AITaskPref extends LitElement {
|
||||
</span>
|
||||
<ha-entity-picker
|
||||
data-name="gen_data_entity_id"
|
||||
.hass=${this.hass}
|
||||
.disabled=${this._prefs === undefined &&
|
||||
isComponentLoaded(this.hass.config, "ai_task")}
|
||||
.value=${this._gen_data_entity_id ||
|
||||
@@ -119,7 +118,6 @@ export class AITaskPref extends LitElement {
|
||||
</span>
|
||||
<ha-entity-picker
|
||||
data-name="gen_image_entity_id"
|
||||
.hass=${this.hass}
|
||||
.disabled=${this._prefs === undefined &&
|
||||
isComponentLoaded(this.hass.config, "ai_task")}
|
||||
.value=${this._gen_image_entity_id ||
|
||||
|
||||
@@ -50,7 +50,6 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.description"
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isNavigationClick } from "../../../common/dom/is-navigation-click";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -91,7 +92,11 @@ class PanelDeveloperTools extends LitElement {
|
||||
panel=${tab.panel}
|
||||
.active=${page === tab.panel}
|
||||
>
|
||||
${this.hass.localize(tab.translationKey)}
|
||||
<a
|
||||
href="/config/developer-tools/${tab.panel}"
|
||||
@click=${this._handleTabAnchorClick}
|
||||
>${this.hass.localize(tab.translationKey)}</a
|
||||
>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
@@ -105,6 +110,14 @@ class PanelDeveloperTools extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTabAnchorClick(ev: MouseEvent) {
|
||||
ev.stopPropagation();
|
||||
const href = isNavigationClick(ev);
|
||||
if (href) {
|
||||
navigate(href);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePageSelected(ev: CustomEvent<{ name: string }>) {
|
||||
const newPage = ev.detail.name;
|
||||
if (!newPage) {
|
||||
@@ -142,6 +155,16 @@ class PanelDeveloperTools extends LitElement {
|
||||
--ha-tab-indicator-color: var(--app-header-text-color, white);
|
||||
--ha-tab-track-color: transparent;
|
||||
}
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
ha-tab-group-tab a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1em 1.5em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -220,7 +220,6 @@ class HaPanelDevState extends LitElement {
|
||||
<div class="inputs">
|
||||
<ha-entity-picker
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.value=${this._entityId}
|
||||
@value-changed=${this._entityIdChanged}
|
||||
show-entity-id
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import {
|
||||
@@ -12,27 +12,10 @@ import {
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../../../data/context";
|
||||
import type {
|
||||
DeviceAction,
|
||||
DeviceCondition,
|
||||
DeviceTrigger,
|
||||
} from "../../../../data/device/device_automation";
|
||||
import {
|
||||
fetchDeviceActions,
|
||||
fetchDeviceConditions,
|
||||
fetchDeviceTriggers,
|
||||
sortDeviceAutomations,
|
||||
} from "../../../../data/device/device_automation";
|
||||
import type { ScriptConfig } from "../../../../data/script";
|
||||
import { showScriptEditor } from "../../../../data/script";
|
||||
import { showSceneEditor } from "../../../../data/scene";
|
||||
import "../../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
@@ -48,21 +31,11 @@ import {
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { DeviceAddToDialogParams } from "./show-dialog-device-add-to";
|
||||
|
||||
type DeviceLegacyAddToActionType =
|
||||
| "trigger"
|
||||
| "condition"
|
||||
| "automation_action"
|
||||
| "script_action";
|
||||
|
||||
type DeviceAddToAction =
|
||||
| (AddToActionListItem & {
|
||||
kind: "add-to";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & {
|
||||
kind: "legacy";
|
||||
legacyType: DeviceLegacyAddToActionType;
|
||||
})
|
||||
| (AddToActionListItem & { kind: "scene" });
|
||||
|
||||
@customElement("dialog-device-add-to")
|
||||
@@ -75,76 +48,25 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state() private _params?: DeviceAddToDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _triggers?: DeviceTrigger[];
|
||||
|
||||
@state() private _conditions?: DeviceCondition[];
|
||||
|
||||
@state() private _actions?: DeviceAction[];
|
||||
|
||||
public showDialog(params: DeviceAddToDialogParams): void {
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole check can be removed.
|
||||
if (!params.newTriggersConditions && this._api) {
|
||||
this._fetchDeviceAutomations(params);
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole check can be removed.
|
||||
if (
|
||||
changedProps.has("_api") &&
|
||||
this._api &&
|
||||
this._params &&
|
||||
!this._params.newTriggersConditions &&
|
||||
!this._triggers
|
||||
) {
|
||||
this._fetchDeviceAutomations(this._params);
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
protected firstUpdated() {
|
||||
this._i18n.loadBackendTranslation("device_automation");
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private async _fetchDeviceAutomations(
|
||||
params: DeviceAddToDialogParams
|
||||
): Promise<void> {
|
||||
const deviceId = params.device.id;
|
||||
|
||||
const [triggers, conditions, actions] = await Promise.all([
|
||||
fetchDeviceTriggers(this._api.callWS, deviceId),
|
||||
fetchDeviceConditions(this._api.callWS, deviceId),
|
||||
fetchDeviceActions(this._api.callWS, deviceId),
|
||||
]);
|
||||
|
||||
this._triggers = triggers.sort(sortDeviceAutomations);
|
||||
this._conditions = conditions.sort(sortDeviceAutomations);
|
||||
this._actions = actions.sort(sortDeviceAutomations);
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._triggers = undefined;
|
||||
this._conditions = undefined;
|
||||
this._actions = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -168,14 +90,12 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
${this._params.newTriggersConditions
|
||||
? this._renderNewOptions()
|
||||
: this._renderLegacyOptions()}
|
||||
${this._renderOptions()}
|
||||
</ha-adaptive-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNewOptions() {
|
||||
private _renderOptions() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -238,112 +158,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private _renderLegacyOptions() {
|
||||
if (!this._triggers && !this._conditions && !this._actions) {
|
||||
return html`
|
||||
<div class="loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hasTriggers = Boolean(this._triggers?.length);
|
||||
const hasConditions = Boolean(this._conditions?.length);
|
||||
const hasActions = Boolean(this._actions?.length);
|
||||
const hasScenes = Boolean(this._params.entityIds.length);
|
||||
|
||||
if (!hasTriggers && !hasConditions && !hasActions && !hasScenes) {
|
||||
return html`
|
||||
<div class="empty">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_device_automations"
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const automationActions: DeviceAddToAction[] = [];
|
||||
if (hasTriggers) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasConditions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasActions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const scriptActions: DeviceAddToAction[] = hasActions
|
||||
? [
|
||||
{
|
||||
kind: "legacy",
|
||||
legacyType: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
),
|
||||
actions: automationActions,
|
||||
empty: automationActions.length
|
||||
? undefined
|
||||
: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
),
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: scriptActions,
|
||||
empty: scriptActions.length
|
||||
? undefined
|
||||
: this._i18n.localize("ui.panel.config.devices.script.no_scripts"),
|
||||
},
|
||||
];
|
||||
this._addSceneSection(sections);
|
||||
|
||||
return html`
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addSceneSection(
|
||||
sections: AddToActionListSection<DeviceAddToAction>[]
|
||||
): void {
|
||||
@@ -380,12 +194,7 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.kind === "add-to") {
|
||||
this._handleAddToAction(action.key);
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleLegacyAction(action.legacyType);
|
||||
this._handleAddToAction(action.key);
|
||||
}
|
||||
|
||||
private _handleAddToAction(key: AddToAutomationScriptActionKey) {
|
||||
@@ -397,30 +206,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
addToActionHandler(key, { device_id: this._params.device.id });
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private _handleLegacyAction(type: DeviceLegacyAddToActionType) {
|
||||
this.closeDialog();
|
||||
|
||||
if (type === "script_action") {
|
||||
const newScript = {} as ScriptConfig;
|
||||
if (this._actions?.length) {
|
||||
newScript.sequence = [this._actions[0]];
|
||||
}
|
||||
showScriptEditor(newScript, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger" && this._triggers?.length) {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
} else if (type === "condition" && this._conditions?.length) {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
} else if (type === "automation_action" && this._actions?.length) {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
if (!this._params) {
|
||||
return;
|
||||
@@ -439,12 +224,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
padding: var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DeviceRegistryEntry } from "../../../../data/device/device_registr
|
||||
|
||||
export interface DeviceAddToDialogParams {
|
||||
device: DeviceRegistryEntry;
|
||||
newTriggersConditions: boolean;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomCurrentTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
@@ -65,13 +62,12 @@ import {
|
||||
disableConfigEntry,
|
||||
sortConfigEntries,
|
||||
} from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import {
|
||||
removeConfigEntryFromDevice,
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../../data/device/device_registry";
|
||||
import { subscribeLabFeature } from "../../../data/labs";
|
||||
import type { DiagnosticInfo } from "../../../data/diagnostics";
|
||||
import {
|
||||
fetchDiagnosticHandler,
|
||||
@@ -207,10 +203,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _deviceAlertsActionsTimeout?: number;
|
||||
|
||||
@state() private _newTriggersConditions = false;
|
||||
|
||||
private _unsubLabFeature?: (() => void) | undefined;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg: EntityRegistryEntry[] = [];
|
||||
@@ -380,15 +372,13 @@ export class HaConfigDevicePage extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
loadDeviceRegistryDetailDialog();
|
||||
this._subscribeLabFeature();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._findRelated();
|
||||
// Broadcast device context for quick bar
|
||||
fireEvent(this, "hass-related-context", {
|
||||
fireRelatedContext(this, {
|
||||
itemType: "device",
|
||||
itemId: this.deviceId,
|
||||
});
|
||||
@@ -398,7 +388,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
clearTimeout(this._deviceAlertsActionsTimeout);
|
||||
this._unsubLabFeature?.();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -925,7 +914,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.time=${this._logbookTime}
|
||||
.entityIds=${this._entityIds(entities)}
|
||||
.deviceIds=${this._deviceIdInList(this.deviceId)}
|
||||
.scope=${"device"}
|
||||
name-detail="entity"
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
@@ -1408,7 +1397,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
);
|
||||
showDeviceAddToDialog(this, {
|
||||
device,
|
||||
newTriggersConditions: this._newTriggersConditions,
|
||||
entityIds: sceneEntityIds,
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
@@ -1416,23 +1404,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private _subscribeLabFeature() {
|
||||
if (!isComponentLoaded(this.hass.config, "automation")) {
|
||||
return;
|
||||
}
|
||||
subscribeLabFeature(
|
||||
this.hass.connection,
|
||||
"automation",
|
||||
"new_triggers_conditions",
|
||||
(feature) => {
|
||||
this._newTriggersConditions = feature.enabled;
|
||||
}
|
||||
).then((unsub) => {
|
||||
this._unsubLabFeature = unsub;
|
||||
});
|
||||
}
|
||||
|
||||
private _renderIntegrationInfo(
|
||||
device: DeviceRegistryEntry,
|
||||
integrations: ConfigEntry[],
|
||||
|
||||
@@ -859,7 +859,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-states"]?.value}
|
||||
.states=${this._states(this.hass.localize)}
|
||||
.label=${this.hass.localize("ui.panel.config.devices.picker.state")}
|
||||
|
||||
@@ -269,7 +269,6 @@ export class DialogEnergyGasSettings
|
||||
: this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -328,7 +328,6 @@ export class DialogEnergyGridSettings
|
||||
${this._importCostType === "entity"
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.cost_entity_label"
|
||||
@@ -416,7 +415,6 @@ export class DialogEnergyGridSettings
|
||||
${this._exportCostType === "entity"
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._source.entity_energy_price_export}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.grid.dialog.compensation_entity_label"
|
||||
|
||||
@@ -230,7 +230,6 @@ export class DialogEnergyWaterSettings
|
||||
: this._costs === "entity"
|
||||
? html`<ha-entity-picker
|
||||
class="price-options"
|
||||
.hass=${this.hass}
|
||||
include-domains='["sensor", "input_number"]'
|
||||
.value=${this._source.entity_energy_price}
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -747,7 +747,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
SCANNER_SOURCE_TYPES.includes(stateObj?.attributes?.source_type)
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._associatedZone}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.associated_zone"
|
||||
|
||||
@@ -1019,7 +1019,6 @@ export class HaConfigEntities extends LitElement {
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.headers.status"
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { animationStyles } from "../../../../../resources/theme/animations.globals";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
@@ -21,6 +23,7 @@ import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
@@ -66,20 +69,46 @@ class ZHAConfigDashboard extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this.hass.loadBackendTranslation("config_panel", "zha", false);
|
||||
this._fetchConfigEntry();
|
||||
this._fetchConfiguration();
|
||||
this._fetchDevicesAndGroups();
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
if (!isComponentLoaded(this.hass.config, "zha")) {
|
||||
navigate("/config/integrations", { replace: true });
|
||||
return;
|
||||
}
|
||||
this.hass.loadBackendTranslation("config_panel", "zha", false);
|
||||
this._load();
|
||||
}
|
||||
|
||||
private async _load(): Promise<void> {
|
||||
await this._fetchConfigEntry();
|
||||
if (!this._configEntry) {
|
||||
return;
|
||||
}
|
||||
this._fetchConfiguration();
|
||||
this._fetchDevicesAndGroups();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const devices = this._configEntry
|
||||
? Object.values(this.hass.devices).filter((device) =>
|
||||
device.config_entries.includes(this._configEntry!.entry_id)
|
||||
)
|
||||
: [];
|
||||
if (!this._configEntry) {
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.zha.network.caption")}
|
||||
back-path="/config"
|
||||
>
|
||||
<div class="loading">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
const configEntry = this._configEntry;
|
||||
const devices = Object.values(this.hass.devices).filter((device) =>
|
||||
device.config_entries.includes(configEntry.entry_id)
|
||||
);
|
||||
const deviceCount = devices.length;
|
||||
|
||||
let entityCount = 0;
|
||||
@@ -463,6 +492,12 @@ class ZHAConfigDashboard extends LitElement {
|
||||
margin-top: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-12);
|
||||
}
|
||||
|
||||
ha-md-list {
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
-1
@@ -252,7 +252,6 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
return html`
|
||||
<div>
|
||||
<ha-qr-scanner
|
||||
.hass=${this.hass}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
@qr-code-closed=${this.closeDialog}
|
||||
@qr-code-more-options=${this._qrScanShowMoreOptions}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user