mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-16 00:57:34 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 776ae2416c |
@@ -1,244 +0,0 @@
|
||||
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: ${{ !cancelled() }}
|
||||
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,16 +54,8 @@ 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/
|
||||
|
||||
+34
-36
@@ -1,3 +1,4 @@
|
||||
/* global require, module, __dirname, process */
|
||||
const path = require("path");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
@@ -83,7 +84,12 @@ module.exports.swcOptions = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
|
||||
module.exports.babelOptions = ({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw,
|
||||
}) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
assumptions: {
|
||||
@@ -96,22 +102,14 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
|
||||
[
|
||||
"@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"),
|
||||
{
|
||||
@@ -119,14 +117,32 @@ module.exports.babelOptions = ({ latestBuild, isTestBuild, sw }) => ({
|
||||
ignoreModuleNotFound: true,
|
||||
},
|
||||
],
|
||||
// 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`.
|
||||
// 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
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{ version: dependencies["@babel/runtime"], moduleName: "@babel/runtime" },
|
||||
{ version: dependencies["@babel/runtime"] },
|
||||
],
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-private-methods",
|
||||
@@ -305,22 +321,4 @@ 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,13 +1,9 @@
|
||||
// @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,3 +1,4 @@
|
||||
/* 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,10 +45,3 @@ gulp.task(
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-e2e-test-app",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.e2eTestApp_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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,3 +1,4 @@
|
||||
/* global process */
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import {
|
||||
@@ -267,24 +268,3 @@ 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
|
||||
)
|
||||
);
|
||||
|
||||
@@ -201,23 +201,3 @@ 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,7 +4,6 @@ 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, readdir } from "fs/promises";
|
||||
import { readFile, access } from "fs/promises";
|
||||
import { generateLicenseFile } from "generate-license-file";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
@@ -11,98 +11,58 @@ 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.join(NODE_MODULES, "echarts/NOTICE")];
|
||||
const NOTICE_FILES = [
|
||||
path.resolve(paths.root_dir, "node_modules/echarts/NOTICE"),
|
||||
];
|
||||
|
||||
// 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).
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers 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 pinned version is no longer installed.
|
||||
// that the new version's license still matches. The build will fail
|
||||
// if the installed version does not match the pinned version.
|
||||
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",
|
||||
licenseFile: "license-mit",
|
||||
licensePath: path.resolve(
|
||||
paths.root_dir,
|
||||
"node_modules/type-fest/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, licenseFile } of LICENSE_OVERRIDES) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const packageDir = await findPackageDir(packageName, version);
|
||||
for (const { packageName, version, licensePath } of LICENSE_OVERRIDES) {
|
||||
const pkgJsonPath = path.resolve(
|
||||
paths.root_dir,
|
||||
`node_modules/${packageName}/package.json`
|
||||
);
|
||||
|
||||
if (!packageDir) {
|
||||
let packageJSON;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJSON = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
||||
} catch {
|
||||
throw new Error(
|
||||
`License override for "${packageName}" is pinned to version ${version}, but that version is not installed. ` +
|
||||
`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}. ` +
|
||||
`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,7 +14,6 @@ import {
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
@@ -232,22 +231,3 @@ 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,12 +48,6 @@ 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`));
|
||||
@@ -65,16 +59,16 @@ for (const buildType of ["Modern", "Legacy"]) {
|
||||
console.log(detailsClose);
|
||||
|
||||
// Manually log the Core-JS polyfills using the same technique
|
||||
if (corejsOpts) {
|
||||
if (presetEnvOpts.useBuiltIns) {
|
||||
console.log(detailsOpen(`${buildType} Build Core-JS Polyfills`));
|
||||
const targets = compilationTargets.default(babelOpts?.targets, {
|
||||
browserslistEnv,
|
||||
});
|
||||
const polyfillList = coreJSCompat({ targets }).list.filter(
|
||||
polyfillFilter(
|
||||
corejsOpts.method,
|
||||
corejsOpts.proposals,
|
||||
corejsOpts.shippedProposals
|
||||
`${presetEnvOpts.useBuiltIns}-global`,
|
||||
presetEnvOpts?.corejs?.proposals,
|
||||
presetEnvOpts?.shippedProposals
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/* global module */
|
||||
|
||||
module.exports = function litDisableDevModeLoader(source) {
|
||||
return source.replace(
|
||||
/\b(const|let|var) DEV_MODE = true;/g,
|
||||
"$1 DEV_MODE = false;"
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
/* 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.
|
||||
//
|
||||
// `keepClosingSlash` is required for `svg`` templates: SVG elements such as
|
||||
// `<path />` and `<circle />` are not void elements in HTML, so dropping the
|
||||
// trailing slash would break the markup. It is harmless for HTML.
|
||||
const htmlOptions = {
|
||||
caseSensitive: true,
|
||||
collapseWhitespace: true,
|
||||
conservativeCollapse: true,
|
||||
decodeEntities: true,
|
||||
keepClosingSlash: 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,15 +50,4 @@ 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"
|
||||
),
|
||||
};
|
||||
|
||||
+19
-88
@@ -1,3 +1,4 @@
|
||||
/* global require, module, __dirname */
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
@@ -47,12 +48,6 @@ 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",
|
||||
@@ -72,42 +67,25 @@ const createRspackConfig = ({
|
||||
{
|
||||
test: /\.m?js$|\.ts$/,
|
||||
exclude: /node_modules[\\/]core-js/,
|
||||
use: (info) =>
|
||||
[
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({
|
||||
latestBuild,
|
||||
isTestBuild,
|
||||
sw: info.issuerLayer === "sw",
|
||||
}),
|
||||
cacheDirectory: !isProdBuild,
|
||||
cacheCompression: false,
|
||||
},
|
||||
use: (info) => [
|
||||
{
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw: info.issuerLayer === "sw",
|
||||
}),
|
||||
cacheDirectory: !isProdBuild,
|
||||
cacheCompression: false,
|
||||
},
|
||||
// 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),
|
||||
},
|
||||
{
|
||||
loader: "builtin:swc-loader",
|
||||
options: bundle.swcOptions(),
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
@@ -154,47 +132,6 @@ 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 })
|
||||
),
|
||||
@@ -401,11 +338,6 @@ 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,
|
||||
@@ -413,5 +345,4 @@ module.exports = {
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
};
|
||||
|
||||
@@ -53,7 +53,6 @@ const CONFIG_PANEL_COMMANDS = [
|
||||
"config/scene/config",
|
||||
"search/related",
|
||||
"tag/list",
|
||||
"assist_pipeline/",
|
||||
];
|
||||
|
||||
@customElement("ha-demo")
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { AssistPipeline } from "../../../src/data/assist_pipeline";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const pipelines: AssistPipeline[] = [
|
||||
{
|
||||
id: "01home_assistant_cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
language: "en",
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: "en",
|
||||
stt_engine: "cloud",
|
||||
stt_language: "en-US",
|
||||
tts_engine: "cloud",
|
||||
tts_language: "en-US",
|
||||
tts_voice: "JennyNeural",
|
||||
wake_word_entity: null,
|
||||
wake_word_id: null,
|
||||
},
|
||||
{
|
||||
id: "01local",
|
||||
name: "Local",
|
||||
language: "en",
|
||||
conversation_engine: "conversation.home_assistant",
|
||||
conversation_language: "en",
|
||||
stt_engine: "stt.faster_whisper",
|
||||
stt_language: "en",
|
||||
tts_engine: "tts.piper",
|
||||
tts_language: "en",
|
||||
tts_voice: null,
|
||||
wake_word_entity: null,
|
||||
wake_word_id: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockAssist = (hass: MockHomeAssistant) => {
|
||||
// Stub for assist pipeline list — returns a cloud and a local pipeline so the
|
||||
// voice assistants config panel shows configured assistants.
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines,
|
||||
preferred_pipeline: "01home_assistant_cloud",
|
||||
}));
|
||||
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { mockApplicationCredentials } from "./application_credentials";
|
||||
import { mockAssist } from "./assist";
|
||||
import { mockAutomation } from "./automation";
|
||||
import { mockBackup } from "./backup";
|
||||
import { mockBlueprint } from "./blueprint";
|
||||
@@ -38,5 +37,4 @@ export const mockConfigPanel = (hass: MockHomeAssistant) => {
|
||||
mockScene(hass);
|
||||
mockSearch(hass);
|
||||
mockTags(hass);
|
||||
mockAssist(hass);
|
||||
};
|
||||
|
||||
@@ -2,27 +2,22 @@ import type { ExposeEntitySettings } from "../../../src/data/expose";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const exposedEntities: Record<string, ExposeEntitySettings> = {
|
||||
"light.floor_lamp": {
|
||||
"light.bed_light": {
|
||||
conversation: true,
|
||||
"cloud.alexa": true,
|
||||
"cloud.google_assistant": true,
|
||||
},
|
||||
"light.living_room_spotlights": {
|
||||
"light.ceiling_lights": {
|
||||
conversation: true,
|
||||
"cloud.alexa": true,
|
||||
"cloud.google_assistant": false,
|
||||
},
|
||||
"light.bar_lamp": {
|
||||
"switch.decorative_lights": {
|
||||
conversation: true,
|
||||
"cloud.alexa": false,
|
||||
"cloud.google_assistant": true,
|
||||
},
|
||||
"light.kitchen_spotlights": {
|
||||
conversation: true,
|
||||
"cloud.alexa": true,
|
||||
"cloud.google_assistant": true,
|
||||
},
|
||||
"light.outdoor_light": {
|
||||
"climate.ecobee": {
|
||||
conversation: true,
|
||||
"cloud.alexa": true,
|
||||
"cloud.google_assistant": true,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockUpdate = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("update/list", () => []);
|
||||
};
|
||||
@@ -234,12 +234,6 @@ export default tseslint.config(
|
||||
globals: globals.serviceworker,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["test/e2e/*.mjs"],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { DemoTrace } from "./types";
|
||||
|
||||
export const notTriggeredTrace: DemoTrace = {
|
||||
trace: {
|
||||
last_step: "trigger/0",
|
||||
run_id: "788767ce152d3d4475134bf1107986d4",
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
timestamp: {
|
||||
start: "2021-03-25T04:36:51.223337+00:00",
|
||||
finish: "2021-03-25T04:36:51.223341+00:00",
|
||||
},
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
domain: "automation",
|
||||
item_id: "1781703842452",
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
timestamp: "2021-03-25T04:36:51.223340+00:00",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: {
|
||||
entity_id: "light.bed_light",
|
||||
to_state: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
config: {
|
||||
id: "1781703842452",
|
||||
alias: "Light Turned On Notification",
|
||||
description: "Send a notification when a specific light is turned on.",
|
||||
triggers: [
|
||||
{
|
||||
trigger: "light.turned_on",
|
||||
target: {
|
||||
floor_id: "test",
|
||||
},
|
||||
options: {
|
||||
for: "00:00:00",
|
||||
behavior: "each",
|
||||
},
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
actions: [
|
||||
{
|
||||
action: "notify.notify",
|
||||
data: {
|
||||
message: "A light was turned on.",
|
||||
},
|
||||
},
|
||||
],
|
||||
mode: "single",
|
||||
},
|
||||
context: {
|
||||
id: "01KVAX7CG7XBDYGJYAGA4XJHGX",
|
||||
parent_id: "01KVAX7CG631JRX4H3JS5JJ11Q",
|
||||
user_id: null,
|
||||
},
|
||||
},
|
||||
logbookEntries: [],
|
||||
};
|
||||
@@ -24,33 +24,6 @@ const traces: DemoTrace[] = [
|
||||
error: 'Variable "beer" cannot be None',
|
||||
}),
|
||||
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
|
||||
mockDemoTrace({
|
||||
state: "stopped",
|
||||
script_execution: "not_triggered",
|
||||
not_triggered: true,
|
||||
// Not-triggered traces have no trigger description.
|
||||
trigger: null,
|
||||
trace: {
|
||||
"trigger/0": [
|
||||
{
|
||||
path: "trigger/0",
|
||||
changed_variables: {
|
||||
trigger: {
|
||||
id: "0",
|
||||
idx: "0",
|
||||
alias: null,
|
||||
platform: "light.turned_on",
|
||||
},
|
||||
},
|
||||
result: {
|
||||
reason: "new_state_not_a_match",
|
||||
data: { entity_id: "light.bed_light", to_state: "off" },
|
||||
},
|
||||
timestamp: "2021-03-25T04:36:51.223693+00:00",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-automation-trace-timeline")
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/trace/ha-trace-path-details";
|
||||
import type { HatScriptGraph } from "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-script-graph";
|
||||
import "../../../../src/components/trace/hat-trace-timeline";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { basicTrace } from "../../data/traces/basic_trace";
|
||||
import { motionLightTrace } from "../../data/traces/motion-light-trace";
|
||||
import { notTriggeredTrace } from "../../data/traces/not-triggered-trace";
|
||||
import type { DemoTrace } from "../../data/traces/types";
|
||||
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace, notTriggeredTrace];
|
||||
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
|
||||
|
||||
@customElement("demo-automation-trace")
|
||||
export class DemoAutomationTrace extends LitElement {
|
||||
@@ -23,25 +20,18 @@ export class DemoAutomationTrace extends LitElement {
|
||||
|
||||
@state() private _selected = {};
|
||||
|
||||
@queryAll("hat-script-graph") private _graphs!: NodeListOf<HatScriptGraph>;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
${traces.map((trace, idx) => {
|
||||
const graph = this._graphs?.[idx];
|
||||
const selectedPath = this._selected[idx];
|
||||
const selectedNode = selectedPath
|
||||
? graph?.renderedNodes[selectedPath]
|
||||
: undefined;
|
||||
return html`
|
||||
${traces.map(
|
||||
(trace, idx) => html`
|
||||
<ha-card .header=${trace.trace.config.alias}>
|
||||
<div class="card-content">
|
||||
<hat-script-graph
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedPath}
|
||||
.selected=${this._selected[idx]}
|
||||
@graph-node-selected=${this._handleGraphNodeSelected}
|
||||
.sampleIdx=${idx}
|
||||
></hat-script-graph>
|
||||
@@ -50,25 +40,15 @@ export class DemoAutomationTrace extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.selectedPath=${selectedPath}
|
||||
.selectedPath=${this._selected[idx]}
|
||||
@value-changed=${this._handleTimelineValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></hat-trace-timeline>
|
||||
${selectedNode && graph
|
||||
? html`<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedNode}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.trackedNodes=${graph.trackedNodes}
|
||||
.renderedNodes=${graph.renderedNodes}
|
||||
></ha-trace-path-details>`
|
||||
: nothing}
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ class DemoThermostatEntity extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -18,9 +17,7 @@ export class DemoMiscBoxShadow extends LitElement {
|
||||
(size) => html`
|
||||
<div
|
||||
class="box"
|
||||
style=${styleMap({
|
||||
boxShadow: `var(--ha-box-shadow-${size})`,
|
||||
})}
|
||||
style="box-shadow: var(--ha-box-shadow-${size})"
|
||||
>
|
||||
${size}
|
||||
</div>
|
||||
|
||||
@@ -353,6 +353,7 @@ export class DemoEntityState extends LitElement {
|
||||
title: "Icon",
|
||||
template: (entry) => html`
|
||||
<state-badge
|
||||
.hass=${hass}
|
||||
.stateObj=${entry.stateObj}
|
||||
.stateColor=${true}
|
||||
></state-badge>
|
||||
|
||||
@@ -151,7 +151,7 @@ class DemoMoreInfoClimate extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
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, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
+9
-18
@@ -22,18 +22,13 @@
|
||||
"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: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"
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
@@ -132,11 +127,10 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ampproject/remapping": "2.3.0",
|
||||
"@babel/core": "8.0.1",
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/helper-define-polyfill-provider": "1.0.0",
|
||||
"@babel/plugin-transform-runtime": "8.0.1",
|
||||
"@babel/preset-env": "8.0.2",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.62.0",
|
||||
@@ -144,11 +138,9 @@
|
||||
"@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",
|
||||
@@ -159,13 +151,13 @@
|
||||
"@types/leaflet-draw": "1.0.13",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.2",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@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-polyfill-corejs3": "1.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
@@ -190,12 +182,11 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.8",
|
||||
"lint-staged": "17.0.7",
|
||||
"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",
|
||||
@@ -225,6 +216,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.17.0",
|
||||
"volta": {
|
||||
"node": "24.18.0"
|
||||
"node": "24.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260624.5"
|
||||
version = "20260527.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
+2
-9
@@ -111,7 +111,7 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
|
||||
]);
|
||||
|
||||
/** Domains that use a timestamp for state. */
|
||||
const TIMESTAMP_STATE_DOMAINS_LIST = [
|
||||
export const TIMESTAMP_STATE_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
@@ -127,14 +127,7 @@ const TIMESTAMP_STATE_DOMAINS_LIST = [
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
] as const;
|
||||
|
||||
export type TimestampStateDomain =
|
||||
(typeof TIMESTAMP_STATE_DOMAINS_LIST)[number];
|
||||
|
||||
export const TIMESTAMP_STATE_DOMAINS = new Set<string>(
|
||||
TIMESTAMP_STATE_DOMAINS_LIST
|
||||
);
|
||||
]);
|
||||
|
||||
/** Temperature units. */
|
||||
export const UNIT_C = "°C";
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import type { HaDurationData } from "../../components/ha-duration-input";
|
||||
|
||||
export default function durationToSeconds(duration: string): number {
|
||||
const parts = duration.split(":").map(Number);
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
}
|
||||
|
||||
export const durationDataToSeconds = (duration: HaDurationData): number =>
|
||||
(duration.days || 0) * 86400 +
|
||||
(duration.hours || 0) * 3600 +
|
||||
(duration.minutes || 0) * 60 +
|
||||
(duration.seconds || 0) +
|
||||
(duration.milliseconds || 0) / 1000;
|
||||
|
||||
@@ -125,15 +125,7 @@ export interface EntityPickerDisplay {
|
||||
}
|
||||
|
||||
export const computeEntityPickerDisplay = (
|
||||
hass: Pick<
|
||||
HomeAssistant,
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "areas"
|
||||
| "floors"
|
||||
| "language"
|
||||
| "translationMetadata"
|
||||
>,
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity
|
||||
): EntityPickerDisplay => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
@@ -8,30 +8,14 @@ export interface RelatedIdSets {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Build a set of related IDs for a given related result.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @param current - The queried item to merge in.
|
||||
* @returns The related ID sets, including the current item.
|
||||
* @returns The related ID sets.
|
||||
*/
|
||||
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] : []),
|
||||
]),
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-progress-button";
|
||||
import { apiContext } from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { Appearance } from "../ha-button";
|
||||
|
||||
@customElement("ha-call-service-button")
|
||||
class HaCallServiceButton extends LitElement {
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@@ -58,7 +56,7 @@ class HaCallServiceButton extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||
|
||||
try {
|
||||
await this._api.callService(
|
||||
await this.hass.callService(
|
||||
this.domain,
|
||||
this.service,
|
||||
this.data,
|
||||
|
||||
@@ -51,6 +51,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public colors?: Record<
|
||||
string,
|
||||
string | undefined
|
||||
@@ -180,6 +182,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
return html`${title}${datapoints.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const context = this.entityContext?.[entityId];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
@@ -207,7 +210,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName
|
||||
? html`${param.seriesName}: `
|
||||
: nothing}${value}${statSuffix}`;
|
||||
: nothing}${value}${context
|
||||
? html`<br /><span style="opacity: 0.6"
|
||||
>${" ".repeat(2)}${context}</span
|
||||
>`
|
||||
: nothing}${statSuffix}`;
|
||||
})}`;
|
||||
};
|
||||
|
||||
@@ -445,7 +452,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) => {
|
||||
const label = formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
const width = measureTextWidth(label, 12) + 5;
|
||||
|
||||
@@ -33,6 +33,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property() public identifier?: string;
|
||||
@@ -145,8 +147,18 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const entityId = seriesName
|
||||
? this._chartData.find((d) => (d.name as string) === seriesName)?.id
|
||||
: undefined;
|
||||
const context = entityId
|
||||
? this.entityContext?.[entityId as string]
|
||||
: undefined;
|
||||
return html`${seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing}${context
|
||||
? html`<div style="text-align: center; opacity: 0.6; margin: 0 0 4px">
|
||||
${context}
|
||||
</div>`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${String(color ?? "")}
|
||||
.rtl=${rtl}
|
||||
|
||||
@@ -58,6 +58,8 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public names?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public entityContext?: Record<string, string>;
|
||||
|
||||
@property({ attribute: false }) public colors?: Record<
|
||||
string,
|
||||
string | undefined
|
||||
@@ -197,6 +199,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.endTime=${this._computedEndTime}
|
||||
.paddingYAxis=${this._maxYWidth}
|
||||
.names=${this.names}
|
||||
.entityContext=${this.entityContext}
|
||||
.colors=${this.colors}
|
||||
.chartIndex=${index}
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@@ -220,6 +223,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.endTime=${this._computedEndTime}
|
||||
.showNames=${this.showNames}
|
||||
.names=${this.names}
|
||||
.entityContext=${this.entityContext}
|
||||
.narrow=${this.narrow}
|
||||
.chunked=${this.virtualize}
|
||||
.paddingYAxis=${this._maxYWidth}
|
||||
|
||||
@@ -552,7 +552,6 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _formatYAxisLabel = (value: number) =>
|
||||
formatNumber(value, this.hass.locale, {
|
||||
minimumFractionDigits: value === 0 ? 0 : this._yAxisFractionDigits,
|
||||
maximumFractionDigits: this._yAxisFractionDigits,
|
||||
});
|
||||
|
||||
|
||||
@@ -1463,12 +1463,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -16,12 +15,7 @@ import {
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { CallWS, HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import {
|
||||
DEFAULT_ROW_RENDERER_CONTENT,
|
||||
type PickerComboBoxItem,
|
||||
} from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -111,7 +105,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(value, this._automations)}
|
||||
@value-changed=${this._automationChanged}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
@@ -167,13 +160,6 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
);
|
||||
|
||||
// Device automation labels (entity name + subtype) are often longer than the
|
||||
// field, so let the option wrap onto multiple lines instead of truncating.
|
||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) =>
|
||||
html`<ha-combo-box-item type="button" compact multiline>
|
||||
${DEFAULT_ROW_RENDERER_CONTENT(item)}
|
||||
</ha-combo-box-item>`;
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const automation = this._automations?.find(
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
} from "../../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistant, 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;
|
||||
@@ -85,6 +87,10 @@ 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}
|
||||
@@ -99,6 +105,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
<div class="entity">
|
||||
<ha-entity-picker
|
||||
.curValue=${entityId}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
@@ -126,6 +133,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
</ha-sortable>
|
||||
<div>
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeEntities=${this.includeEntities}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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";
|
||||
@@ -7,21 +6,10 @@ 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";
|
||||
@@ -45,21 +33,7 @@ const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@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>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
@@ -157,25 +131,12 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@state() private _pendingEntityId?: string;
|
||||
|
||||
@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) {
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (
|
||||
this._pendingEntityId &&
|
||||
changedProperties.has("_states") &&
|
||||
this._states[this._pendingEntityId]
|
||||
changedProperties.has("hass") &&
|
||||
this.hass.states !== changedProperties.get("hass")?.states &&
|
||||
this.hass.states[this._pendingEntityId]
|
||||
) {
|
||||
this._setValue(this._pendingEntityId);
|
||||
this._pendingEntityId = undefined;
|
||||
@@ -185,7 +146,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._i18n.loadBackendTranslation("title");
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _findExtraOption(value: string | undefined) {
|
||||
@@ -196,11 +157,15 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _renderExtraOptionStart(extraOption: EntitySelectorExtraOption) {
|
||||
const stateObj = extraOption.entity_id
|
||||
? this._states[extraOption.entity_id]
|
||||
? this.hass.states[extraOption.entity_id]
|
||||
: undefined;
|
||||
if (stateObj) {
|
||||
return html`
|
||||
<state-badge slot="start" .stateObj=${stateObj}></state-badge>
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`;
|
||||
}
|
||||
if (extraOption.icon_path) {
|
||||
@@ -232,7 +197,7 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const stateObj = this._states[entityId];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
@@ -246,23 +211,23 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
const { primary, secondary } = computeEntityPickerDisplay(
|
||||
{
|
||||
...this._registries,
|
||||
language: this._i18n.language,
|
||||
translationMetadata: this._i18n.translationMetadata,
|
||||
},
|
||||
this.hass,
|
||||
stateObj
|
||||
);
|
||||
|
||||
return html`
|
||||
<state-badge .stateObj=${stateObj} slot="start"></state-badge>
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
<span slot="headline">${primary}</span>
|
||||
<span slot="supporting-text">${secondary}</span>
|
||||
`;
|
||||
};
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.showEntityId || this._config.userData?.showEntityIdPicker;
|
||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
|
||||
@@ -285,6 +250,7 @@ export class HaEntityPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
@@ -310,14 +276,17 @@ export class HaEntityPicker extends LitElement {
|
||||
};
|
||||
|
||||
private _getAdditionalItems = () =>
|
||||
this._getCreateItems(this._i18n.localize, this.createDomains);
|
||||
this._getCreateItems(this.hass.localize, this.createDomains);
|
||||
|
||||
private _getCreateItems = memoizeOne(
|
||||
(localize: LocalizeFunc, createDomains: this["createDomains"]) => {
|
||||
(
|
||||
localize: this["hass"]["localize"],
|
||||
createDomains: this["createDomains"]
|
||||
) => {
|
||||
if (!createDomains?.length) {
|
||||
return [];
|
||||
}
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
return createDomains.map((domain) => {
|
||||
const primary = localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
@@ -342,9 +311,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
registries: ContextType<typeof registriesContext>,
|
||||
i18n: ContextType<typeof internationalizationContext>,
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
@@ -354,46 +321,21 @@ export class HaEntityPicker extends LitElement {
|
||||
excludeEntities?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
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
|
||||
)
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const entityItems = this._getEntitiesMemoized(
|
||||
this._states,
|
||||
this._registries,
|
||||
this._i18n,
|
||||
const items = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.entityFilter,
|
||||
@@ -403,23 +345,14 @@ 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._states[opt.entity_id] : undefined,
|
||||
stateObj: opt.entity_id ? this.hass.states[opt.entity_id] : undefined,
|
||||
}));
|
||||
return [...resolvedExtras, ...sortedItems];
|
||||
return [...resolvedExtras, ...items];
|
||||
}
|
||||
return sortedItems;
|
||||
return items;
|
||||
};
|
||||
|
||||
private _shouldHideClearIcon() {
|
||||
@@ -429,10 +362,11 @@ export class HaEntityPicker extends LitElement {
|
||||
protected render() {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this._i18n.localize("ui.components.entity.entity-picker.placeholder");
|
||||
this.hass.localize("ui.components.entity.entity-picker.placeholder");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.autofocus=${this.autofocus}
|
||||
.allowCustomValue=${this.allowCustomEntity}
|
||||
@@ -450,13 +384,12 @@ 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._i18n.localize("ui.components.entity.entity-picker.add"))
|
||||
this.hass.localize("ui.components.entity.entity-picker.add"))
|
||||
: undefined}
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -469,23 +402,17 @@ 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 = items.findIndex(
|
||||
const index = filteredItems.findIndex(
|
||||
(item) => item.stateObj?.entity_id === search
|
||||
);
|
||||
if (index === -1) {
|
||||
return items;
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = items.splice(index, 1);
|
||||
items.unshift(exactMatch);
|
||||
return items;
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
public async open() {
|
||||
@@ -509,7 +436,7 @@ export class HaEntityPicker extends LitElement {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) {
|
||||
if (this._states[item.entityId]) {
|
||||
if (this.hass.states[item.entityId]) {
|
||||
this._setValue(item.entityId);
|
||||
} else {
|
||||
this._pendingEntityId = item.entityId;
|
||||
@@ -535,7 +462,7 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _notFoundLabel = (search: string) =>
|
||||
this._i18n.localize("ui.components.entity.entity-picker.no_match", {
|
||||
this.hass.localize("ui.components.entity.entity-picker.no_match", {
|
||||
term: html`<b>‘${search}’</b>`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFlash, mdiFlashOff } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -7,9 +6,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { apiContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { forwardHaptic } from "../../data/haptics";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-switch";
|
||||
@@ -30,8 +29,8 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api?: ContextType<typeof apiContext>;
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@@ -119,7 +118,7 @@ export class HaEntityToggle extends LitElement {
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
private async _callService(turnOn): Promise<void> {
|
||||
if (!this._api || !this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic(this, "light");
|
||||
@@ -150,7 +149,7 @@ export class HaEntityToggle extends LitElement {
|
||||
this._isOn = turnOn;
|
||||
|
||||
try {
|
||||
await this._api.callService(serviceDomain, service, {
|
||||
await this.hass.callService(serviceDomain, service, {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
@@ -8,19 +6,14 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
|
||||
import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import {
|
||||
consumeEntityRegistryEntry,
|
||||
consumeLocalize,
|
||||
} from "../../common/decorators/consume-context-entry";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { unitFromParts, valueFromParts } from "../../common/entity/value_parts";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { formattersContext } from "../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-label-badge";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@@ -48,15 +41,7 @@ const getTruncatedKey = (domainKey: string, stateKey: string) => {
|
||||
|
||||
@customElement("ha-state-label-badge")
|
||||
export class HaStateLabelBadge extends LitElement {
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consumeEntityRegistryEntry({ entityIdPath: ["state", "entity_id"] })
|
||||
private _entry?: EntityRegistryDisplayEntry;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public state?: HassEntity;
|
||||
|
||||
@@ -93,8 +78,10 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return html`
|
||||
<ha-label-badge
|
||||
class="warning"
|
||||
label=${this._localize("state_badge.default.error")}
|
||||
description=${this._localize("state_badge.default.entity_not_found")}
|
||||
label=${this.hass!.localize("state_badge.default.error")}
|
||||
description=${this.hass!.localize(
|
||||
"state_badge.default.entity_not_found"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
@@ -108,7 +95,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
// 4. Icon determined via entity state
|
||||
// 5. Value string as fallback
|
||||
const domain = computeStateDomain(entityState);
|
||||
const entry = this._entry;
|
||||
const entry = this.hass?.entities[entityState.entity_id];
|
||||
|
||||
const showIcon =
|
||||
this.icon || this._computeShowIcon(domain, entityState, entry);
|
||||
@@ -188,12 +175,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return "—";
|
||||
}
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return valueFromParts(
|
||||
this._formatters.formatEntityStateToParts(entityState)
|
||||
);
|
||||
return valueFromParts(this.hass!.formatEntityStateToParts(entityState));
|
||||
}
|
||||
|
||||
private _computeShowIcon(
|
||||
@@ -228,11 +210,11 @@ export class HaStateLabelBadge extends LitElement {
|
||||
) {
|
||||
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
|
||||
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
|
||||
return this._localize(`state_badge.default.${entityState.state}`);
|
||||
return this.hass!.localize(`state_badge.default.${entityState.state}`);
|
||||
}
|
||||
const domainStateKey = getTruncatedKey(domain, entityState.state);
|
||||
if (domainStateKey) {
|
||||
return this._localize(`state_badge.${domainStateKey}`);
|
||||
return this.hass!.localize(`state_badge.${domainStateKey}`);
|
||||
}
|
||||
// Person and device tracker state can be zone name
|
||||
if (domain === "person" || domain === "device_tracker") {
|
||||
@@ -241,12 +223,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
if (!this._formatters) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
unitFromParts(this._formatters.formatEntityStateToParts(entityState)) ||
|
||||
null
|
||||
unitFromParts(this.hass!.formatEntityStateToParts(entityState)) || null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -343,7 +343,11 @@ export class HaStatisticPicker extends LitElement {
|
||||
return html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge .stateObj=${item.stateObj} slot="start"></state-badge>
|
||||
<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
`
|
||||
: item.icon_path
|
||||
? html`
|
||||
@@ -484,6 +488,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -15,12 +14,13 @@ import {
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { connectionContext } from "../../data/context";
|
||||
import { isBrandUrl } from "../../util/brands-url";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
export class StateBadge extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public overrideIcon?: string;
|
||||
@@ -36,10 +36,6 @@ export class StateBadge extends LitElement {
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public icon = true;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state() private _iconStyle: Record<string, string | undefined> = {};
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -110,15 +106,14 @@ export class StateBadge extends LitElement {
|
||||
></ha-state-icon>`;
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
!changedProps.has("stateObj") &&
|
||||
!changedProps.has("overrideImage") &&
|
||||
!changedProps.has("overrideIcon") &&
|
||||
!changedProps.has("stateColor") &&
|
||||
!changedProps.has("color") &&
|
||||
!changedProps.has("_connection")
|
||||
!changedProps.has("color")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -138,10 +133,12 @@ export class StateBadge extends LitElement {
|
||||
stateObj.attributes.entity_picture) &&
|
||||
!this.overrideIcon
|
||||
) {
|
||||
let imageUrl = this._resolveImageUrl(
|
||||
let imageUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture
|
||||
);
|
||||
stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
if (domain === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
@@ -182,7 +179,11 @@ export class StateBadge extends LitElement {
|
||||
}
|
||||
}
|
||||
} else if (this.overrideImage) {
|
||||
backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`;
|
||||
let imageUrl = this.overrideImage;
|
||||
if (this.hass) {
|
||||
imageUrl = this.hass.hassUrl(imageUrl);
|
||||
}
|
||||
backgroundImage = `url(${imageUrl})`;
|
||||
this.icon = false;
|
||||
}
|
||||
}
|
||||
@@ -191,20 +192,6 @@ export class StateBadge extends LitElement {
|
||||
this.style.backgroundImage = backgroundImage;
|
||||
}
|
||||
|
||||
// Sign the image URL via the connection context so brand images
|
||||
// (/api/brands/...) get their access token. Without a way to sign, a brands
|
||||
// request would be rejected (and logged/blocked by core), so skip it until
|
||||
// we can sign.
|
||||
private _resolveImageUrl(url: string | undefined): string {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
if (this._connection) {
|
||||
return this._connection.hassUrl(url);
|
||||
}
|
||||
return isBrandUrl(url) ? "" : url;
|
||||
}
|
||||
|
||||
protected getClass() {
|
||||
const cls = new Map(
|
||||
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
|
||||
|
||||
@@ -24,6 +24,7 @@ class StateInfo extends LitElement {
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.stateColor=${true}
|
||||
.color=${this.color}
|
||||
|
||||
@@ -173,6 +173,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
domainItems = multiTermSortedSearch(
|
||||
domainItems,
|
||||
searchString,
|
||||
this._domainSearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -225,6 +226,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
entityItems = multiTermSortedSearch(
|
||||
entityItems,
|
||||
searchString,
|
||||
this._entitySearchKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areasContext, floorsContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
@@ -18,13 +17,7 @@ export interface AreasDisplayValue {
|
||||
|
||||
@customElement("ha-areas-display-editor")
|
||||
export class HaAreasDisplayEditor extends LitElement {
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areas!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floors!: ContextType<typeof floorsContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -42,10 +35,10 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areas = Object.values(this._areas);
|
||||
const areas = Object.values(this.hass.areas);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area, this._floors);
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
return {
|
||||
value: area.area_id,
|
||||
label: area.name,
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
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, state } from "lit/decorators";
|
||||
import { customElement, property } 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 { ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -34,17 +30,7 @@ const UNASSIGNED_FLOOR = "__unassigned__";
|
||||
|
||||
@customElement("ha-areas-floors-display-editor")
|
||||
export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
@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({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -65,14 +51,13 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const groupedAreasItems = this._groupedAreasItems(
|
||||
this._areas,
|
||||
this._floors
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const filteredFloors = this._sortedFloors(
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).filter(
|
||||
(floor) =>
|
||||
// Only include floors that have areas assigned to them
|
||||
@@ -139,14 +124,15 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _groupedAreasItems = memoizeOne(
|
||||
(
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
// update items if floors change
|
||||
_hassFloors: HomeAssistant["floors"]
|
||||
): Record<string, DisplayItem[]> => {
|
||||
const areaList = Object.values(areas);
|
||||
const areas = Object.values(hassAreas);
|
||||
|
||||
const groupedItems: Record<string, DisplayItem[]> = areaList.reduce(
|
||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||
(acc, area) => {
|
||||
const { floor } = getAreaContext(area, floors);
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
|
||||
if (!acc[floorId]) {
|
||||
@@ -169,24 +155,23 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
|
||||
private _sortedFloors = memoizeOne(
|
||||
(
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
order: string[] | undefined,
|
||||
localize: LocalizeFunc
|
||||
hassFloors: HomeAssistant["floors"],
|
||||
order: string[] | undefined
|
||||
): FloorRegistryEntry[] => {
|
||||
const sortedFloors = getFloors(floors, order);
|
||||
const noFloors = sortedFloors.length === 0;
|
||||
sortedFloors.push({
|
||||
const floors = getFloors(hassFloors, order);
|
||||
const noFloors = floors.length === 0;
|
||||
floors.push({
|
||||
floor_id: UNASSIGNED_FLOOR,
|
||||
name: noFloors
|
||||
? localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
? this.hass.localize("ui.panel.lovelace.strategy.areas.areas")
|
||||
: this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"),
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
return sortedFloors;
|
||||
return floors;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -195,9 +180,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
const floorIds = this._sortedFloors(
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).map((floor) => floor.floor_id);
|
||||
const newOrder = [...floorIds];
|
||||
const movedFloorId = newOrder.splice(oldIndex, 1)[0];
|
||||
@@ -220,9 +204,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
const floorIds = this._sortedFloors(
|
||||
this._floors,
|
||||
this.value?.floors_display?.order,
|
||||
this._localize
|
||||
this.hass.floors,
|
||||
this.value?.floors_display?.order
|
||||
).map((floor) => floor.floor_id);
|
||||
|
||||
const oldAreaDisplay = this.value?.areas_display ?? {};
|
||||
@@ -240,14 +223,14 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
continue;
|
||||
}
|
||||
const hidden = oldHidden.filter((areaId) => {
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (hidden?.length) {
|
||||
newHidden.push(...hidden);
|
||||
}
|
||||
const order = oldOrder.filter((areaId) => {
|
||||
const id = this._areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
const id = this.hass.areas[areaId]?.floor_id ?? UNASSIGNED_FLOOR;
|
||||
return id === floorId;
|
||||
});
|
||||
if (order?.length) {
|
||||
|
||||
@@ -7,11 +7,6 @@ export class HaComboBoxItem extends HaMdListItem {
|
||||
@property({ type: Boolean, reflect: true, attribute: "border-top" })
|
||||
public borderTop = false;
|
||||
|
||||
// Allow the headline/supporting text to wrap onto multiple lines instead of
|
||||
// truncating with an ellipsis. Off by default to preserve single-line rows.
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public multiline = false;
|
||||
|
||||
static override styles = [
|
||||
...haMdListStyles,
|
||||
css`
|
||||
@@ -46,10 +41,6 @@ export class HaComboBoxItem extends HaMdListItem {
|
||||
font-size: var(--ha-font-size-s);
|
||||
white-space: nowrap;
|
||||
}
|
||||
:host([multiline]) [slot="headline"],
|
||||
:host([multiline]) [slot="supporting-text"] {
|
||||
white-space: normal;
|
||||
}
|
||||
::slotted(state-badge),
|
||||
::slotted(img) {
|
||||
width: 32px;
|
||||
|
||||
@@ -135,6 +135,8 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
private _escapePressed = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener(
|
||||
@@ -288,6 +290,7 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Escape") {
|
||||
this._escapePressed = true;
|
||||
if (this.preventScrimClose) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
@@ -297,23 +300,13 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleHide(ev: DialogHideEvent) {
|
||||
// Ignore wa-hide events bubbling up from nested overlays (pickers,
|
||||
// popovers, bottom sheets, nested dialogs); only handle this dialog's own
|
||||
// hide, like the sibling wa-* handlers above.
|
||||
if (ev.eventPhase !== Event.AT_TARGET) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceIsDialog = ev.detail?.source === (ev.target as WaDialog).dialog;
|
||||
|
||||
// A dialog-sourced hide (Escape, native cancel, or scrim) while the host
|
||||
// still wants the dialog open is a user dismissal, not a programmatic
|
||||
// close. Block it when closing must be guarded, regardless of where focus
|
||||
// currently is — e.g. after a picker overlay took focus and dropped it
|
||||
// outside the dialog, the Escape keydown never reaches this element.
|
||||
if (this.preventScrimClose && sourceIsDialog && this.open) {
|
||||
if (this.preventScrimClose && this._escapePressed && sourceIsDialog) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this._escapePressed = false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
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, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeEntityStates } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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";
|
||||
|
||||
@@ -23,21 +15,7 @@ export interface EntitiesDisplayValue {
|
||||
|
||||
@customElement("ha-entities-display-editor")
|
||||
export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@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({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -54,13 +32,20 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const items = this._items(
|
||||
this.entitiesIds,
|
||||
this._entityStates,
|
||||
this._entitiesReg,
|
||||
this._config,
|
||||
this._connection
|
||||
);
|
||||
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 value: DisplayValue = {
|
||||
order: this.value?.order ?? [],
|
||||
@@ -76,31 +61,6 @@ 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;
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiDelete, mdiFileUpload } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { bytesToString } from "../util/bytes-to-string";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
@@ -27,17 +22,10 @@ declare global {
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() @consumeLocalize() private _localize?: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
@@ -92,7 +80,7 @@ export class HaFileUpload extends LitElement {
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
const localize = this.localize || this._localize!;
|
||||
const localize = this.localize || this.hass!.localize;
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<div class="container">
|
||||
@@ -107,8 +95,8 @@ export class HaFileUpload extends LitElement {
|
||||
>
|
||||
${this.progress
|
||||
? html`<div class="progress">
|
||||
${this.progress}${this._locale &&
|
||||
blankBeforePercent(this._locale)}%
|
||||
${this.progress}${this.hass &&
|
||||
blankBeforePercent(this.hass!.locale)}%
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
const deviceClassToName = (deviceClass: string): string =>
|
||||
deviceClass.charAt(0).toUpperCase() + deviceClass.slice(1).replace(/_/g, " ");
|
||||
|
||||
@customElement("ha-filter-device-classes")
|
||||
export class HaFilterDeviceClasses extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
@query("ha-list") private _list?: HTMLElement;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.history.filter.device_class")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@selected=${this._handleItemSelected}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._deviceClasses(this.hass.states, this._filter),
|
||||
(i) => i,
|
||||
(deviceClass) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${deviceClass}
|
||||
.selected=${(this.value || []).includes(deviceClass)}
|
||||
>
|
||||
${deviceClassToName(deviceClass)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _deviceClasses = memoizeOne(
|
||||
(states: HomeAssistant["states"], filter?: string) => {
|
||||
const deviceClasses = new Set<string>();
|
||||
Object.values(states).forEach((stateObj) => {
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
if (deviceClass) {
|
||||
deviceClasses.add(deviceClass);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(deviceClasses.values())
|
||||
.filter(
|
||||
(deviceClass) =>
|
||||
!filter ||
|
||||
deviceClass.toLowerCase().includes(filter) ||
|
||||
deviceClassToName(deviceClass).toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
deviceClassToName(a),
|
||||
deviceClassToName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const deviceClasses = this._deviceClasses(this.hass.states, this._filter);
|
||||
|
||||
const visible = new Set(deviceClasses);
|
||||
const preserved = (this.value || []).filter((d) => !visible.has(d));
|
||||
const selected = [...ev.detail.index]
|
||||
.map((i) => deviceClasses[i])
|
||||
.filter((d): d is string => !!d);
|
||||
|
||||
this.value = [...preserved, ...selected];
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: var(--ha-border-radius-square);
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-device-classes": HaFilterDeviceClasses;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
devicesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
@@ -32,24 +24,7 @@ interface HaFilterDevicesItem extends HaListVirtualizedItem {
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: devicesContext, subscribe: true })
|
||||
@state()
|
||||
private _devicesReg!: ContextType<typeof devicesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -100,7 +75,7 @@ export class HaFilterDevices extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.devices.caption")}
|
||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -120,13 +95,7 @@ export class HaFilterDevices extends LitElement {
|
||||
</ha-input-search>
|
||||
<ha-list-selectable-virtualized
|
||||
multi
|
||||
.rows=${this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.rows=${this._devices(this.hass.devices, this._filter || "")}
|
||||
.rowRenderer=${this._renderItem}
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
@@ -152,24 +121,13 @@ export class HaFilterDevices extends LitElement {
|
||||
private _handleAdded(ev: CustomEvent<number>) {
|
||||
this.value = [
|
||||
...(this.value ?? []),
|
||||
this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id,
|
||||
this._devices(this.hass.devices, this._filter || "")[ev.detail].id,
|
||||
];
|
||||
}
|
||||
|
||||
private _handleRemoved(ev: CustomEvent<number>) {
|
||||
const id = this._devices(
|
||||
this._devicesReg,
|
||||
this._filter || "",
|
||||
this._localize,
|
||||
this._states,
|
||||
this._i18n.locale.language
|
||||
)[ev.detail].id;
|
||||
const id = this._devices(this.hass.devices, this._filter || "")[ev.detail]
|
||||
.id;
|
||||
this.value = (this.value ?? []).filter((deviceId) => deviceId !== id);
|
||||
}
|
||||
|
||||
@@ -195,24 +153,27 @@ export class HaFilterDevices extends LitElement {
|
||||
|
||||
private _devices = memoizeOne(
|
||||
(
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
filter: string,
|
||||
localize: LocalizeFunc,
|
||||
states: ContextType<typeof statesContext>,
|
||||
language: string | undefined
|
||||
devices: HomeAssistant["devices"],
|
||||
filter: string
|
||||
): HaFilterDevicesItem[] => {
|
||||
const values = Object.values(devices);
|
||||
return values
|
||||
.map((device) => ({
|
||||
id: device.id,
|
||||
interactive: true,
|
||||
name: computeDeviceNameDisplay(device, localize, states),
|
||||
name: computeDeviceNameDisplay(
|
||||
device,
|
||||
this.hass.localize,
|
||||
this.hass.states
|
||||
),
|
||||
}))
|
||||
.filter(
|
||||
({ name }) =>
|
||||
!filter || name.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -233,7 +194,7 @@ export class HaFilterDevices extends LitElement {
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this._api, "device", deviceId));
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
}
|
||||
}
|
||||
const results = await Promise.all(relatedPromises);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -6,14 +5,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, 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 { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, statesContext } from "../data/context";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
@@ -23,17 +20,7 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -56,7 +43,7 @@ export class HaFilterDomains extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.domains.caption")}
|
||||
${this.hass.localize("ui.panel.config.domains.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -78,13 +65,7 @@ export class HaFilterDomains extends LitElement {
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
this._domains(this.hass.states, this._filter, this.value),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
@@ -97,7 +78,7 @@ export class HaFilterDomains extends LitElement {
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this._localize, domain)}
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
@@ -106,34 +87,26 @@ export class HaFilterDomains extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
localize: LocalizeFunc,
|
||||
language: string | undefined,
|
||||
filter: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
private _domains = memoizeOne((states, filter, _value) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
.map((entry) => entry.domain);
|
||||
}
|
||||
);
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
|
||||
.map((entry) => entry.domain);
|
||||
});
|
||||
|
||||
protected updated(changed: PropertyValues<this>) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
@@ -156,13 +129,7 @@ export class HaFilterDomains extends LitElement {
|
||||
}
|
||||
|
||||
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
const domains = this._domains(
|
||||
this._states,
|
||||
this._localize,
|
||||
this._i18n.locale.language,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
const domains = this._domains(this.hass.states, this._filter, this.value);
|
||||
|
||||
const visibleDomains = new Set(domains);
|
||||
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
@@ -29,20 +22,7 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@@ -82,7 +62,7 @@ export class HaFilterEntities extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.entities.caption")}
|
||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -102,10 +82,9 @@ export class HaFilterEntities extends LitElement {
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this._states,
|
||||
this.hass.states,
|
||||
this.type,
|
||||
this._filter || "",
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
@@ -184,10 +163,9 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
private _entities = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
states: HomeAssistant["states"],
|
||||
type: this["type"],
|
||||
filter: string,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) => {
|
||||
const values = Object.values(states);
|
||||
@@ -202,7 +180,11 @@ export class HaFilterEntities extends LitElement {
|
||||
.includes(filter))
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(computeStateName(a), computeStateName(b), language)
|
||||
stringCompare(
|
||||
computeStateName(a),
|
||||
computeStateName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -221,7 +203,7 @@ export class HaFilterEntities extends LitElement {
|
||||
|
||||
for (const entityId of this.value) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this._api, "entity", entityId));
|
||||
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -6,21 +5,14 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
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 { computeRTL } from "../common/util/compute_rtl";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||
import type { RelatedResult } from "../data/search";
|
||||
import { findRelated } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
@@ -34,24 +26,7 @@ import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: areasContext, subscribe: true })
|
||||
@state()
|
||||
private _areasReg!: ContextType<typeof areasContext>;
|
||||
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
@state()
|
||||
private _floorsReg!: ContextType<typeof floorsContext>;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
floors?: string[];
|
||||
@@ -80,7 +55,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this._areasReg, this._floorsReg);
|
||||
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
@@ -90,7 +65,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.areas.caption")}
|
||||
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||
${this.value?.areas?.length || this.value?.floors?.length
|
||||
? html`<div class="badge">
|
||||
${(this.value?.areas?.length || 0) +
|
||||
@@ -110,7 +85,9 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
multi
|
||||
@ha-list-item-selected=${this._handleAdded}
|
||||
@ha-list-item-deselected=${this._handleRemoved}
|
||||
aria-label=${this._localize("ui.panel.config.areas.caption")}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.config.areas.caption"
|
||||
)}
|
||||
>
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
@@ -164,8 +141,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
@@ -248,10 +225,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(
|
||||
areaReg: ContextType<typeof areasContext>,
|
||||
floorReg: ContextType<typeof floorsContext>
|
||||
) => {
|
||||
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
@@ -287,7 +261,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.areas) {
|
||||
for (const areaId of this.value.areas) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this._api, "area", areaId));
|
||||
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +269,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
if (this.value.floors) {
|
||||
for (const floorId of this.value.floors) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this._api, "floor", floorId));
|
||||
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { consume } from "@lit/context";
|
||||
import type { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -6,14 +6,13 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { internationalizationContext, labelsContext } from "../data/context";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
@@ -26,20 +25,14 @@ import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
@state()
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
@@ -52,12 +45,7 @@ export class HaFilterLabels extends LitElement {
|
||||
|
||||
private _filteredLabels = memoizeOne(
|
||||
// `_value` used to recalculate the memoization when the selection changes
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
filter: string | undefined,
|
||||
language: string | undefined,
|
||||
_value
|
||||
) =>
|
||||
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
|
||||
labels
|
||||
.filter(
|
||||
(label) =>
|
||||
@@ -66,7 +54,11 @@ export class HaFilterLabels extends LitElement {
|
||||
label.label_id.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(a.name || a.label_id, b.name || b.label_id, language)
|
||||
stringCompare(
|
||||
a.name || a.label_id,
|
||||
b.name || b.label_id,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -79,7 +71,7 @@ export class HaFilterLabels extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.labels.caption")}
|
||||
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
@@ -104,7 +96,6 @@ export class HaFilterLabels extends LitElement {
|
||||
this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
@@ -138,7 +129,7 @@ export class HaFilterLabels extends LitElement {
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._localize("ui.panel.config.labels.manage_labels")}
|
||||
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
`;
|
||||
@@ -178,7 +169,6 @@ export class HaFilterLabels extends LitElement {
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels || [],
|
||||
this._filter,
|
||||
this._i18n.locale.language,
|
||||
this.value
|
||||
);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -13,6 +14,8 @@ 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[];
|
||||
|
||||
@@ -8,6 +8,7 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
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";
|
||||
@@ -21,6 +22,8 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
|
||||
@customElement("ha-filter-voice-assistants")
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@@ -75,6 +78,7 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
<voice-assistant-brand-icon
|
||||
slot="graphic"
|
||||
.voiceAssistantId=${voiceAssistantId}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</voice-assistant-brand-icon>
|
||||
${voiceAssistants[voiceAssistantId].name}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext, formattersContext } from "../data/context";
|
||||
import "./ha-button";
|
||||
import type { LawnMowerEntity, LawnMowerEntityState } from "../data/lawn_mower";
|
||||
import { LawnMowerEntityFeature } from "../data/lawn_mower";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
interface LawnMowerAction {
|
||||
action: string;
|
||||
@@ -42,19 +39,13 @@ const LAWN_MOWER_ACTIONS: Partial<
|
||||
|
||||
@customElement("ha-lawn_mower-action-button")
|
||||
class HaLawnMowerActionButton extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LawnMowerEntity;
|
||||
|
||||
public render() {
|
||||
const action = LAWN_MOWER_ACTIONS[this.stateObj.state];
|
||||
const state = this.stateObj.state;
|
||||
const action = LAWN_MOWER_ACTIONS[state];
|
||||
|
||||
if (action && supportsFeature(this.stateObj, action.feature)) {
|
||||
return html`
|
||||
@@ -64,14 +55,14 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
.service=${action.service}
|
||||
size="s"
|
||||
>
|
||||
${this._localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
${this.hass.localize(`ui.card.lawn_mower.actions.${action.action}`)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-button appearance="plain" disabled>
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
@@ -80,7 +71,7 @@ class HaLawnMowerActionButton extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = ev.target.service;
|
||||
this._api.callService("lawn_mower", service, {
|
||||
this.hass.callService("lawn_mower", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
items = multiTermSortedSearch(
|
||||
items,
|
||||
searchString,
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
@@ -492,6 +492,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
|
||||
@@ -78,6 +78,7 @@ export class HaPictureUpload extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.icon=${mdiImagePlus}
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.picture-upload.label")}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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";
|
||||
@@ -9,11 +8,9 @@ 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";
|
||||
@@ -36,13 +33,7 @@ prepareZXingModule({
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -115,7 +106,7 @@ class HaQrScanner extends LitElement {
|
||||
${this._error || this._warning}
|
||||
${this._error
|
||||
? html`<ha-button @click=${this._retry} slot="action">
|
||||
${this._localize("ui.components.qr-scanner.retry")}
|
||||
${this.hass.localize("ui.components.qr-scanner.retry")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
@@ -135,7 +126,7 @@ class HaQrScanner extends LitElement {
|
||||
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this._localize(
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.select_camera"
|
||||
)}
|
||||
.path=${mdiCamera}
|
||||
@@ -155,24 +146,28 @@ class HaQrScanner extends LitElement {
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this._localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this._localize("ui.components.qr-scanner.not_supported")}
|
||||
? this.hass.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.hass.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this._localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
.label=${this._localize("ui.components.qr-scanner.enter_qr_code")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-input>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this._localize("ui.common.submit")}
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
private get _nativeBarcodeScanner(): boolean {
|
||||
return Boolean(this._config.auth.external?.config.hasBarCodeScanner);
|
||||
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
@@ -187,7 +182,7 @@ class HaQrScanner extends LitElement {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._reportError(
|
||||
this._localize("ui.components.qr-scanner.no_camera_found")
|
||||
this.hass.localize("ui.components.qr-scanner.no_camera_found")
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -275,7 +270,7 @@ class HaQrScanner extends LitElement {
|
||||
if (msg.command === "bar_code/scan_result") {
|
||||
if (msg.payload.format !== "qr_code") {
|
||||
this._notifyExternalScanner(
|
||||
this._localize("ui.components.qr-scanner.wrong_code", {
|
||||
this.hass.localize("ui.components.qr-scanner.wrong_code", {
|
||||
format: msg.payload.format,
|
||||
rawValue: msg.payload.rawValue,
|
||||
})
|
||||
@@ -293,17 +288,20 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this._config.auth.external!.fireMessage({
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/scan",
|
||||
payload: {
|
||||
title:
|
||||
this.title || this._localize("ui.components.qr-scanner.app.title"),
|
||||
this.title ||
|
||||
this.hass.localize("ui.components.qr-scanner.app.title"),
|
||||
description:
|
||||
this.description ||
|
||||
this._localize("ui.components.qr-scanner.app.description"),
|
||||
this.hass.localize("ui.components.qr-scanner.app.description"),
|
||||
alternative_option_label:
|
||||
this.alternativeOptionLabel ||
|
||||
this._localize("ui.components.qr-scanner.app.alternativeOptionLabel"),
|
||||
this.hass.localize(
|
||||
"ui.components.qr-scanner.app.alternativeOptionLabel"
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -311,7 +309,7 @@ class HaQrScanner extends LitElement {
|
||||
private _closeExternalScanner() {
|
||||
this._removeListener?.();
|
||||
this._removeListener = undefined;
|
||||
this._config.auth.external!.fireMessage({
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/close",
|
||||
});
|
||||
}
|
||||
@@ -320,7 +318,7 @@ class HaQrScanner extends LitElement {
|
||||
if (!this._nativeBarcodeScanner) {
|
||||
return;
|
||||
}
|
||||
this._config.auth.external!.fireMessage({
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/notify",
|
||||
payload: {
|
||||
message,
|
||||
|
||||
@@ -20,16 +20,18 @@ class HaRelativeTime extends ReactiveElement {
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: HomeAssistantInternationalization;
|
||||
|
||||
private _timeout?: number;
|
||||
private _interval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearTimeout();
|
||||
this._clearInterval();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._updateRelative();
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
}
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
@@ -38,19 +40,31 @@ class HaRelativeTime extends ReactiveElement {
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
super.update(changedProps);
|
||||
if (changedProps.has("datetime")) {
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
private _clearTimeout(): void {
|
||||
if (this._timeout) {
|
||||
window.clearTimeout(this._timeout);
|
||||
this._timeout = undefined;
|
||||
private _clearInterval(): void {
|
||||
if (this._interval) {
|
||||
window.clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateRelative(): void {
|
||||
this._clearTimeout();
|
||||
private _startInterval(): void {
|
||||
this._clearInterval();
|
||||
|
||||
// update every 60 seconds
|
||||
this._interval = window.setInterval(() => this._updateRelative(), 60000);
|
||||
}
|
||||
|
||||
private _updateRelative(): void {
|
||||
if (!this._i18n) {
|
||||
return;
|
||||
}
|
||||
@@ -59,33 +73,23 @@ class HaRelativeTime extends ReactiveElement {
|
||||
this.textContent = this._i18n.localize(
|
||||
"ui.components.relative_time.never"
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
undefined,
|
||||
true,
|
||||
this.format
|
||||
);
|
||||
this.textContent = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
}
|
||||
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
undefined,
|
||||
true,
|
||||
this.format
|
||||
);
|
||||
this.textContent = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
|
||||
// Keep the relative time counting up on its own. Refresh every second
|
||||
// while the difference is still measured in seconds, otherwise every
|
||||
// minute.
|
||||
const secondsDiff = Math.abs(Date.now() - date.getTime()) / 1000;
|
||||
this._timeout = window.setTimeout(
|
||||
() => this._updateRelative(),
|
||||
secondsDiff < 60 ? 1000 : 60000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ 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,6 +4,7 @@ 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 {
|
||||
@@ -14,6 +15,8 @@ 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,6 +63,7 @@ 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}
|
||||
@@ -79,6 +80,7 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-entities-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
|
||||
@@ -37,6 +37,7 @@ export class HaFileSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.accept=${this.selector.file?.accept}
|
||||
.icon=${mdiFile}
|
||||
.label=${this.label}
|
||||
|
||||
@@ -84,6 +84,7 @@ 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,6 +91,7 @@ export class HaMediaSelector extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${entityId}
|
||||
.label=${this.label ||
|
||||
this.hass.localize(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumberSelector } from "../../data/selector";
|
||||
import { isSafari } from "../../util/is_safari";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../input/ha-input";
|
||||
@@ -67,16 +66,6 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// On iOS/iPadOS the numeric and decimal on-screen keypads have no minus key,
|
||||
// so negatives can only be typed with the full "text" keyboard. Other
|
||||
// platforms include a minus on their number keypads, so restrict this
|
||||
// workaround to Safari/WebKit and only when the selector allows negatives
|
||||
// (e.g. numeric_state triggers/conditions).
|
||||
const useTextInputMode =
|
||||
isSafari &&
|
||||
this.selector.number?.min !== undefined &&
|
||||
this.selector.number.min < 0;
|
||||
|
||||
const translationKey = this.selector.number?.translation_key;
|
||||
let unit = this.selector.number?.unit_of_measurement;
|
||||
if (isBox && unit && this.localizeValue && translationKey) {
|
||||
@@ -107,12 +96,10 @@ export class HaNumberSelector extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<ha-input
|
||||
.inputmode=${useTextInputMode
|
||||
? "text"
|
||||
: this.selector.number?.step === "any" ||
|
||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||
? "decimal"
|
||||
: "numeric"}
|
||||
.inputMode=${this.selector.number?.step === "any" ||
|
||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||
? "decimal"
|
||||
: "numeric"}
|
||||
.label=${!isBox ? undefined : this.label}
|
||||
.placeholder=${this.placeholder !== undefined
|
||||
? this.placeholder.toString()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -406,12 +405,11 @@ export class HaSerialPortSelector extends LitElement {
|
||||
}
|
||||
let groupItems: SerialPickerItem[] = grouped[type];
|
||||
if (searchString) {
|
||||
const fuseIndex = Fuse.createIndex(DEFAULT_SEARCH_KEYS, groupItems);
|
||||
groupItems = multiTermSortedSearch(
|
||||
groupItems,
|
||||
searchString,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id
|
||||
);
|
||||
}
|
||||
if (!groupItems.length) {
|
||||
|
||||
@@ -3,13 +3,15 @@ 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 { ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistant, 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;
|
||||
|
||||
@@ -23,6 +23,7 @@ export class HaThemeSelector extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -521,10 +521,10 @@ export class HaServiceControl extends LitElement {
|
||||
${description ? html`<p>${description}</p>` : ""}
|
||||
${this._manifest
|
||||
? html` <a
|
||||
href=${this._manifest.is_built_in && this._value?.action
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/actions/${this._value.action}`
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
@@ -558,6 +558,7 @@ 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(
|
||||
|
||||
@@ -461,7 +461,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<div class="add-target-wrapper">
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
popover-placement="bottom-start"
|
||||
.disabled=${this.disabled}
|
||||
.autofocus=${this.autofocus}
|
||||
.helper=${this.helper}
|
||||
@@ -1117,6 +1116,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
@@ -1233,6 +1233,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${(item as EntityComboBoxItem).stateObj}
|
||||
.hass=${this.hass}
|
||||
></state-badge>
|
||||
`
|
||||
: type === "device" && (item as DevicePickerItem).domain
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
@@ -25,13 +23,7 @@ export class HaThemePicker extends LitElement {
|
||||
@property({ attribute: "include-default", type: Boolean })
|
||||
public includeDefault = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@@ -64,8 +56,8 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
private _getItems = () =>
|
||||
this._getThemeOptions(
|
||||
this._ui?.themes.themes || {},
|
||||
this._i18n?.locale.language || "en",
|
||||
this.hass?.themes.themes || {},
|
||||
this.hass?.locale.language || "en",
|
||||
this.includeDefault
|
||||
);
|
||||
|
||||
@@ -78,10 +70,10 @@ export class HaThemePicker extends LitElement {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.label=${this.label ??
|
||||
this._i18n?.localize("ui.components.theme-picker.theme") ??
|
||||
this.hass?.localize("ui.components.theme-picker.theme") ??
|
||||
"Theme"}
|
||||
.placeholder=${this.noThemeLabel ??
|
||||
this._i18n?.localize("ui.components.theme-picker.no_theme")}
|
||||
this.hass?.localize("ui.components.theme-picker.no_theme")}
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
|
||||
@@ -73,6 +73,7 @@ export class HaThemeSettings extends LitElement {
|
||||
${this.showThemePicker
|
||||
? html`
|
||||
<ha-theme-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.labels?.theme}
|
||||
.noThemeLabel=${this.labels?.noTheme}
|
||||
.value=${themeSettings?.theme || undefined}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiCheckCircleOutline,
|
||||
mdiChevronDown,
|
||||
mdiCircleOffOutline,
|
||||
mdiHelpCircleOutline,
|
||||
mdiProgressClock,
|
||||
mdiProgressWrench,
|
||||
@@ -85,11 +84,6 @@ class HaTracePicker extends LitElement {
|
||||
"ui.panel.config.automation.trace.picker.debugged"
|
||||
);
|
||||
item.icon_path = mdiProgressWrench;
|
||||
} else if (trace.not_triggered) {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.not_triggered"
|
||||
);
|
||||
item.icon_path = mdiCircleOffOutline;
|
||||
} else if (trace.script_execution === "finished") {
|
||||
item.secondary = this.hass.localize(
|
||||
"ui.panel.config.automation.trace.picker.finished",
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { apiContext } from "../data/context";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
|
||||
const STATES_INTERCEPTABLE: Record<
|
||||
@@ -49,10 +46,7 @@ const STATES_INTERCEPTABLE: Record<
|
||||
|
||||
@customElement("ha-vacuum-state")
|
||||
export class HaVacuumState extends LitElement {
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -74,19 +68,19 @@ export class HaVacuumState extends LitElement {
|
||||
}
|
||||
|
||||
private _computeInterceptable(
|
||||
stateString: string,
|
||||
state: string,
|
||||
supportedFeatures: number | undefined
|
||||
) {
|
||||
return stateString in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
return state in STATES_INTERCEPTABLE && supportedFeatures !== 0;
|
||||
}
|
||||
|
||||
private _computeLabel(stateString: string, interceptable: boolean) {
|
||||
private _computeLabel(state: string, interceptable: boolean) {
|
||||
return interceptable
|
||||
? this._localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[stateString].action}`
|
||||
? this.hass.localize(
|
||||
`ui.card.vacuum.actions.${STATES_INTERCEPTABLE[state].action}`
|
||||
)
|
||||
: this._localize(
|
||||
`component.vacuum.entity_component._.state.${stateString}`
|
||||
: this.hass.localize(
|
||||
`component.vacuum.entity_component._.state.${state}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +88,7 @@ export class HaVacuumState extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const stateObj = this.stateObj;
|
||||
const service = STATES_INTERCEPTABLE[stateObj.state].service;
|
||||
await this._api.callService("vacuum", service, {
|
||||
await this.hass.callService("vacuum", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import {
|
||||
configContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
} from "../data/context";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-water_heater-state")
|
||||
export class HaWaterHeaterState extends LitElement {
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale?: FrontendLocaleData;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: HomeAssistantConfig;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@@ -41,16 +16,17 @@ export class HaWaterHeaterState extends LitElement {
|
||||
return html`
|
||||
<div class="target">
|
||||
<span class="state-label label">
|
||||
${this._formatters?.formatEntityState(this.stateObj)}
|
||||
${this.hass.formatEntityState(this.stateObj)}
|
||||
</span>
|
||||
<span class="label">${this._computeTarget()}</span>
|
||||
<span class="label"
|
||||
>${this._computeTarget(this.hass, this.stateObj)}</span
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeTarget() {
|
||||
if (!this._locale || !this._hassConfig || !this.stateObj) return null;
|
||||
const stateObj = this.stateObj;
|
||||
private _computeTarget(hass: HomeAssistant, stateObj: HassEntity) {
|
||||
if (!hass || !stateObj) return null;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
|
||||
if (
|
||||
@@ -59,17 +35,17 @@ export class HaWaterHeaterState extends LitElement {
|
||||
) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.target_temp_low,
|
||||
this._locale
|
||||
this.hass.locale
|
||||
)} – ${formatNumber(
|
||||
stateObj.attributes.target_temp_high,
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${formatNumber(
|
||||
stateObj.attributes.temperature,
|
||||
this._locale
|
||||
)} ${this._hassConfig.config.unit_system.temperature}`;
|
||||
this.hass.locale
|
||||
)} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { uid } from "../../common/util/uid";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../ha-button";
|
||||
@@ -70,25 +69,6 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@query("ha-input[data-last]") private _lastInput?: HaInput;
|
||||
|
||||
// Stable key per row, kept in sync with `value`. Because items are plain
|
||||
// strings we cannot use a WeakMap (as the object-based sortable lists do),
|
||||
// so we track keys in a parallel array. Keys stay fixed while a row is
|
||||
// edited (preserving input focus) and travel with the row when reordered.
|
||||
@state() private _keys: string[] = [];
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value") && this._keys.length !== this._items.length) {
|
||||
// Reconcile keys when `value` is (re)set from outside, reusing existing
|
||||
// keys and minting new ones for added rows. Internal add/remove/reorder
|
||||
// keep `_keys` in sync themselves, so this is skipped in those cases.
|
||||
this._keys = Array.from(
|
||||
{ length: this._items.length },
|
||||
(_, i) => this._keys[i] ?? uid()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@@ -100,7 +80,7 @@ class HaInputMulti extends LitElement {
|
||||
<div class="items">
|
||||
${repeat(
|
||||
this._items,
|
||||
(_item, index) => this._keys[index],
|
||||
(item, index) => `${item}-${index}`,
|
||||
(item, index) => {
|
||||
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
|
||||
return html`
|
||||
@@ -182,7 +162,6 @@ class HaInputMulti extends LitElement {
|
||||
if (this.max != null && this._items.length >= this.max) {
|
||||
return;
|
||||
}
|
||||
this._keys = [...this._keys, uid()];
|
||||
const items = [...this._items, ""];
|
||||
this._fireChanged(items);
|
||||
await this.updateComplete;
|
||||
@@ -215,17 +194,11 @@ class HaInputMulti extends LitElement {
|
||||
const items = [...this._items];
|
||||
const [moved] = items.splice(oldIndex, 1);
|
||||
items.splice(newIndex, 0, moved);
|
||||
// Move the row's key with it so its DOM (and identity) is preserved.
|
||||
const keys = [...this._keys];
|
||||
const [movedKey] = keys.splice(oldIndex, 1);
|
||||
keys.splice(newIndex, 0, movedKey);
|
||||
this._keys = keys;
|
||||
this._fireChanged(items);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
this._keys = this._keys.filter((_, i) => i !== index);
|
||||
const items = [...this._items];
|
||||
items.splice(index, 1);
|
||||
this._fireChanged(items);
|
||||
|
||||
@@ -41,11 +41,7 @@ export class HaInputSearch extends HaInput {
|
||||
...HaInput.styles,
|
||||
css`
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: var(--ha-input-search-height, 40px);
|
||||
border-radius: var(
|
||||
--ha-input-search-border-radius,
|
||||
var(--ha-border-radius-md)
|
||||
);
|
||||
height: 40px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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 { ThemeMode } from "../../types";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
@@ -45,6 +45,8 @@ 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,6 +100,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
: nothing}
|
||||
<div class="content">
|
||||
<ha-media-player-toggle
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
checked
|
||||
disabled
|
||||
@@ -107,6 +108,7 @@ 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}
|
||||
|
||||
@@ -101,6 +101,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
</span>
|
||||
<ha-media-manage-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.currentItem=${this._currentItem}
|
||||
@media-refresh=${this._refreshMedia}
|
||||
></ha-media-manage-button>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiFolderEdit } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { configContext } from "../../data/context";
|
||||
import type { MediaPlayerItem } from "../../data/media-player";
|
||||
import {
|
||||
isLocalMediaSourceContentId,
|
||||
isImageUploadMediaSourceContentId,
|
||||
} from "../../data/media_source";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-button";
|
||||
import { showMediaManageDialog } from "./show-media-manage-dialog";
|
||||
@@ -23,13 +20,7 @@ declare global {
|
||||
|
||||
@customElement("ha-media-manage-button")
|
||||
class MediaManageButton extends LitElement {
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) currentItem?: MediaPlayerItem;
|
||||
|
||||
@@ -40,7 +31,7 @@ class MediaManageButton extends LitElement {
|
||||
!this.currentItem ||
|
||||
!(
|
||||
isLocalMediaSourceContentId(this.currentItem.media_content_id || "") ||
|
||||
(this._config.user?.is_admin &&
|
||||
(this.hass!.user?.is_admin &&
|
||||
isImageUploadMediaSourceContentId(this.currentItem.media_content_id))
|
||||
)
|
||||
) {
|
||||
@@ -49,7 +40,9 @@ class MediaManageButton extends LitElement {
|
||||
return html`
|
||||
<ha-button appearance="filled" size="s" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this._localize("ui.components.media-browser.file_management.manage")}
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
)}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,66 +1,35 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { type CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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 { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import {
|
||||
areasContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
internationalizationContext,
|
||||
} from "../../data/context";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
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: ContextType<typeof entitiesContext>,
|
||||
devices: ContextType<typeof devicesContext>,
|
||||
areas: ContextType<typeof areasContext>,
|
||||
floors: ContextType<typeof floorsContext>,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
isRTL: boolean,
|
||||
stateObj: HassEntity
|
||||
stateObj: HomeAssistant["states"][string]
|
||||
) => {
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
@@ -81,11 +50,7 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this.entityId];
|
||||
|
||||
let icon = mdiSpeaker;
|
||||
if (stateObj.state === "playing") {
|
||||
@@ -95,16 +60,16 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
this._entities,
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._floors,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.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";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
public updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("size")) {
|
||||
switch (this.size) {
|
||||
|
||||
@@ -17,10 +17,9 @@ import type {
|
||||
ChooseActionTraceStep,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import { getDataFromPath, isTriggerPath } from "../../data/trace";
|
||||
import { getDataFromPath } from "../../data/trace";
|
||||
import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tab-group";
|
||||
@@ -70,7 +69,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="padded-box trace-info">
|
||||
${this._renderNotTriggeredNotice()} ${this._renderSelectedTraceInfo()}
|
||||
${this._renderSelectedTraceInfo()}
|
||||
</div>
|
||||
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
@@ -96,22 +95,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderNotTriggeredNotice() {
|
||||
if (
|
||||
!this.trace.not_triggered ||
|
||||
!this.selected?.path ||
|
||||
!isTriggerPath(this.selected.path) ||
|
||||
!(this.selected.path in this.trace.trace)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-alert alert-type="info">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.not_triggered"
|
||||
)}
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
private _renderSelectedTraceInfo() {
|
||||
const paths = this.trace.trace;
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ export class HatGraphNode extends LitElement {
|
||||
@property({ attribute: "not-enabled", reflect: true, type: Boolean })
|
||||
notEnabled = false;
|
||||
|
||||
@property({ attribute: "not-triggered", reflect: true, type: Boolean })
|
||||
notTriggered = false;
|
||||
|
||||
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
||||
graphStart = false;
|
||||
|
||||
@@ -130,9 +127,6 @@ export class HatGraphNode extends LitElement {
|
||||
--stroke-clr: var(--hover-clr);
|
||||
--icon-clr: var(--default-icon-clr);
|
||||
}
|
||||
:host([not-triggered]) circle {
|
||||
stroke-dasharray: 4 3;
|
||||
}
|
||||
:host([not-enabled]) circle {
|
||||
--stroke-clr: var(--disabled-clr);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user