mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-26 18:21:40 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebd8feb801 | |||
| 9e8d38ea63 | |||
| 4fd976dc8c | |||
| f8d8dc4eaa | |||
| 8ccda740ee | |||
| 8528dd8a15 | |||
| ac2f8ebce3 | |||
| 1462f65f5a | |||
| 3e9d3d90a1 | |||
| f28898551b | |||
| eabbcf3a95 | |||
| 01255cebc6 | |||
| d20e062de9 | |||
| 835c0fa35c | |||
| e308272d89 | |||
| d2ae376058 | |||
| fdd57645ee | |||
| 1ef1655a4c | |||
| aa1108fc41 | |||
| e3c6a57080 | |||
| 1e22649ef8 | |||
| 5abd04d09a | |||
| a5bf35690b | |||
| d98eb47490 | |||
| 738e92d27d | |||
| ade2e9272b | |||
| d8ce60dfb6 | |||
| db9374925e | |||
| 1bcd1293c0 | |||
| b8cf061ebb | |||
| 6585da9a73 | |||
| 368df82e97 | |||
| 1d99a5dff9 | |||
| 0ca72b763a | |||
| 31848a1efd |
@@ -1,308 +0,0 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
# BrowserStack runs are gated by the `e2e-browserstack` label or manual
|
||||
# dispatch — see the e2e-browserstack job below. Local Chromium always runs.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-browserstack:
|
||||
description: "Run BrowserStack suite"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
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
|
||||
|
||||
# ── Run Playwright tests on BrowserStack (real devices + browsers) ─────────
|
||||
# The BrowserStack SDK manages the Local tunnel and uploads results to the
|
||||
# BrowserStack Automate dashboard automatically — no tunnel action needed.
|
||||
#
|
||||
# Gated on:
|
||||
# - manual dispatch with the run-browserstack input enabled, OR
|
||||
# - a PR with the `e2e-browserstack` label applied.
|
||||
# This keeps CI fast on normal PRs while still allowing on-demand runs.
|
||||
e2e-browserstack:
|
||||
name: E2E (BrowserStack)
|
||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch' && inputs.run-browserstack) ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'e2e-browserstack'))
|
||||
environment: browserstack
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||
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 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 (BrowserStack)
|
||||
run: yarn test:e2e:browserstack
|
||||
|
||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
||||
# Only depends on the local job — BrowserStack reports live on the
|
||||
# BrowserStack Automate dashboard and don't feed into the local blob report.
|
||||
report:
|
||||
name: Report
|
||||
needs: [e2e-local]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download blob report (local)
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
|
||||
- name: Stage blobs for merge
|
||||
run: node test/e2e/collect-blob-reports.mjs
|
||||
|
||||
- name: Merge blob reports
|
||||
run: npx playwright merge-reports -c test/e2e/playwright.merge.config.ts test/e2e/reports/blob
|
||||
|
||||
- name: Upload merged HTML report
|
||||
id: upload-report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test/e2e/reports/combined/
|
||||
retention-days: 14
|
||||
|
||||
- name: Post report link to PR
|
||||
if: github.event_name == 'pull_request'
|
||||
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 test report\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/
|
||||
|
||||
+325
-325
File diff suppressed because one or more lines are too long
+1
-1
@@ -13,4 +13,4 @@ nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.16.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.17.0.cjs
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# BrowserStack Automate configuration for Home Assistant frontend e2e tests.
|
||||
# Credentials are read from BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY
|
||||
# environment variables set in GitHub Actions (or locally).
|
||||
# See: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs
|
||||
|
||||
userName: ${BROWSERSTACK_USERNAME}
|
||||
accessKey: ${BROWSERSTACK_ACCESS_KEY}
|
||||
|
||||
projectName: Home Assistant Frontend
|
||||
buildName: e2e tests
|
||||
buildIdentifier: "CI #${BUILD_NUMBER}"
|
||||
|
||||
# ── Platforms ────────────────────────────────────────────────────────────────
|
||||
platforms:
|
||||
- os: Windows
|
||||
osVersion: 11
|
||||
browserName: chrome
|
||||
browserVersion: latest
|
||||
- os: OS X
|
||||
osVersion: Ventura
|
||||
browserName: playwright-firefox
|
||||
browserVersion: latest
|
||||
- deviceName: iPad 6th
|
||||
osVersion: 12
|
||||
browserName: playwright-webkit
|
||||
- deviceName: iPhone 12
|
||||
osVersion: 14
|
||||
browserName: playwright-webkit
|
||||
- deviceName: Samsung Galaxy S23
|
||||
osVersion: 13
|
||||
browserName: chrome
|
||||
realMobile: true
|
||||
|
||||
parallelsPerPlatform: 1
|
||||
|
||||
# ── Local tunnel ─────────────────────────────────────────────────────────────
|
||||
# The SDK manages the BrowserStack Local tunnel automatically.
|
||||
browserstackLocal: true
|
||||
|
||||
framework: playwright
|
||||
|
||||
# Pin to the latest Playwright version BrowserStack supports. Our local
|
||||
# @playwright/test is newer (1.59.x) which BrowserStack does not yet support,
|
||||
# causing a "Malformed endpoint" connection error if left unset.
|
||||
# Update this when BrowserStack adds support for a newer version.
|
||||
# Supported versions: https://www.browserstack.com/docs/automate/playwright/browsers-and-os
|
||||
playwrightVersion: 1.latest
|
||||
|
||||
# ── Debugging ────────────────────────────────────────────────────────────────
|
||||
debug: false
|
||||
networkLogs: false
|
||||
consoleLogs: errors
|
||||
testObservability: true
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global require, module, __dirname, process */
|
||||
const path = require("path");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
@@ -320,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(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global require, module, __dirname */
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
@@ -337,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,
|
||||
@@ -349,5 +345,4 @@ module.exports = {
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAssist = (hass: MockHomeAssistant) => {
|
||||
// Stub for assist pipeline list — returns empty so developer tools assist
|
||||
// tab loads without errors.
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines: [],
|
||||
preferred_pipeline: null,
|
||||
}));
|
||||
|
||||
// Stub for assist pipeline run — immediately sends run-end event so
|
||||
// the UI does not hang waiting for a response.
|
||||
hass.mockWS("assist_pipeline/run", (_msg, _hass, onChange) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
type: "run-end",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@@ -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,4 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
@@ -14,6 +15,11 @@ import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../src/data/context";
|
||||
import { updateHassGroups } from "../../../../src/data/context/updateContext";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
@@ -518,6 +524,17 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
// The date/datetime selectors and the date-picker dialog consume these
|
||||
// contexts (provided by the root element in the real app). Provide them here
|
||||
// so they work in the gallery.
|
||||
private _i18nProvider = new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
});
|
||||
|
||||
private _configProvider = new ContextProvider(this, {
|
||||
context: configContext,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
@@ -539,6 +556,16 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
this._i18nProvider.setValue(
|
||||
updateHassGroups.internationalization(this.hass)
|
||||
);
|
||||
this._configProvider.setValue(updateHassGroups.config(this.hass));
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
|
||||
+5
-17
@@ -22,16 +22,7 @@
|
||||
"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:browserstack": "node test/e2e/run-suites.mjs demo:browserstack app:browserstack gallery:browserstack",
|
||||
"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:demo:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:app:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts",
|
||||
"test:e2e:gallery:browserstack": "browserstack-node-sdk 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",
|
||||
@@ -146,11 +137,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.13",
|
||||
"@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",
|
||||
@@ -165,11 +154,10 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.9",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"browserstack-node-sdk": "1.53.2",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
@@ -209,7 +197,7 @@
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.8",
|
||||
"vitest": "4.1.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
@@ -225,8 +213,8 @@
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.16.0",
|
||||
"packageManager": "yarn@4.17.0",
|
||||
"volta": {
|
||||
"node": "24.16.0"
|
||||
"node": "24.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ensureArray } from "../array/ensure-array";
|
||||
import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
isCore(page) || isLoadedIntegration(hass, page);
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
(!page.filter || page.filter(hass));
|
||||
|
||||
export const isLoadedIntegration = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -110,6 +110,25 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
|
||||
"media_player",
|
||||
]);
|
||||
|
||||
/** Domains that use a timestamp for state. */
|
||||
export const TIMESTAMP_STATE_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
/** Temperature units. */
|
||||
export const UNIT_C = "°C";
|
||||
export const UNIT_F = "°F";
|
||||
|
||||
@@ -3,23 +3,24 @@ import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { selectUnit } from "../util/select-unit";
|
||||
|
||||
const formatRelTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto" })
|
||||
(locale: FrontendLocaleData, style: Intl.RelativeTimeFormatStyle) =>
|
||||
new Intl.RelativeTimeFormat(locale.language, { numeric: "auto", style })
|
||||
);
|
||||
|
||||
export const relativeTime = (
|
||||
from: Date,
|
||||
locale: FrontendLocaleData,
|
||||
to?: Date,
|
||||
includeTense = true
|
||||
includeTense = true,
|
||||
style: Intl.RelativeTimeFormatStyle = "long"
|
||||
): string => {
|
||||
const diff = selectUnit(from, to, locale);
|
||||
if (includeTense) {
|
||||
return formatRelTimeMem(locale).format(diff.value, diff.unit);
|
||||
return formatRelTimeMem(locale, style).format(diff.value, diff.unit);
|
||||
}
|
||||
return Intl.NumberFormat(locale.language, {
|
||||
style: "unit",
|
||||
unit: diff.unit,
|
||||
unitDisplay: "long",
|
||||
unitDisplay: style,
|
||||
}).format(Math.abs(diff.value));
|
||||
};
|
||||
|
||||
@@ -60,6 +60,17 @@ export const computeAttributeValueToParts = (
|
||||
return [{ type: "value", value: localize("state.default.unknown") }];
|
||||
}
|
||||
|
||||
// Device class attribute, return the integration's translated name
|
||||
if (attribute === "device_class" && typeof attributeValue === "string") {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const deviceClassName = localize(
|
||||
`component.${domain}.entity_component.${attributeValue}.name`
|
||||
);
|
||||
if (deviceClassName) {
|
||||
return [{ type: "value", value: deviceClassName }];
|
||||
}
|
||||
}
|
||||
|
||||
// Number value, return formatted number
|
||||
if (typeof attributeValue === "number") {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
@@ -30,7 +30,7 @@ export const computeEntityEntryName = (
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
const name =
|
||||
entry.name ||
|
||||
entry.name ??
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
@@ -59,7 +59,8 @@ export const computeEntityEntryName = (
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
// Empty name = main entity → undefined, so callers fall back to the device name.
|
||||
return name || undefined;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
|
||||
@@ -21,29 +21,11 @@ import {
|
||||
isNumericSensorDeviceClass,
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES,
|
||||
} from "../../data/sensor";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../const";
|
||||
|
||||
// Domains whose state is a timezone-agnostic date and/or time string.
|
||||
const DATE_TIME_DOMAINS = new Set(["date", "input_datetime", "time"]);
|
||||
|
||||
// Domains whose state is a timestamp.
|
||||
const TIMESTAMP_DOMAINS = new Set([
|
||||
"ai_task",
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
"datetime",
|
||||
]);
|
||||
|
||||
// Maps Intl.NumberFormat part types to ValuePart types for monetary states.
|
||||
const MONETARY_TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
@@ -273,7 +255,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
TIMESTAMP_DOMAINS.has(domain) ||
|
||||
TIMESTAMP_STATE_DOMAINS.has(domain) ||
|
||||
(domain === "sensor" &&
|
||||
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(attributes.device_class))
|
||||
) {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { supportsFeature } from "./supports-feature";
|
||||
|
||||
export type FeatureClassNames<T extends number = number> = Partial<
|
||||
Record<T, string>
|
||||
>;
|
||||
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export const featureClassNames = (
|
||||
stateObj: HassEntity,
|
||||
classNames: FeatureClassNames
|
||||
) => {
|
||||
if (!stateObj || !stateObj.attributes.supported_features) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) =>
|
||||
supportsFeature(stateObj, Number(feature)) ? classNames[feature] : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { AITaskEntityFeature } from "../../data/ai_task";
|
||||
import { AlarmControlPanelEntityFeature } from "../../data/alarm_control_panel";
|
||||
import { AssistSatelliteEntityFeature } from "../../data/assist_satellite";
|
||||
import { CalendarEntityFeature } from "../../data/calendar";
|
||||
import { CameraEntityFeature } from "../../data/camera";
|
||||
import { ClimateEntityFeature } from "../../data/climate";
|
||||
import { ConversationEntityFeature } from "../../data/conversation";
|
||||
import { CoverEntityFeature } from "../../data/cover";
|
||||
import { FanEntityFeature } from "../../data/fan";
|
||||
import { HumidifierEntityFeature } from "../../data/humidifier";
|
||||
import { LawnMowerEntityFeature } from "../../data/lawn_mower";
|
||||
import { LightEntityFeature } from "../../data/light";
|
||||
import { LockEntityFeature } from "../../data/lock";
|
||||
import { MediaPlayerEntityFeature } from "../../data/media-player";
|
||||
import { NotifyEntityFeature } from "../../data/notify";
|
||||
import { RemoteEntityFeature } from "../../data/remote";
|
||||
import { SirenEntityFeature } from "../../data/siren";
|
||||
import { TodoListEntityFeature } from "../../data/todo";
|
||||
import { UpdateEntityFeature } from "../../data/update";
|
||||
import { VacuumEntityFeature } from "../../data/vacuum";
|
||||
import { ValveEntityFeature } from "../../data/valve";
|
||||
import { WaterHeaterEntityFeature } from "../../data/water_heater";
|
||||
import { WeatherEntityFeature } from "../../data/weather";
|
||||
|
||||
export type FeatureEnum = Record<string | number, string | number>;
|
||||
|
||||
const DOMAIN_ENUMS = {
|
||||
ai_task: AITaskEntityFeature,
|
||||
alarm_control_panel: AlarmControlPanelEntityFeature,
|
||||
assist_satellite: AssistSatelliteEntityFeature,
|
||||
calendar: CalendarEntityFeature,
|
||||
camera: CameraEntityFeature,
|
||||
climate: ClimateEntityFeature,
|
||||
conversation: ConversationEntityFeature,
|
||||
cover: CoverEntityFeature,
|
||||
fan: FanEntityFeature,
|
||||
humidifier: HumidifierEntityFeature,
|
||||
lawn_mower: LawnMowerEntityFeature,
|
||||
light: LightEntityFeature,
|
||||
lock: LockEntityFeature,
|
||||
media_player: MediaPlayerEntityFeature,
|
||||
notify: NotifyEntityFeature,
|
||||
remote: RemoteEntityFeature,
|
||||
siren: SirenEntityFeature,
|
||||
todo: TodoListEntityFeature,
|
||||
update: UpdateEntityFeature,
|
||||
vacuum: VacuumEntityFeature,
|
||||
valve: ValveEntityFeature,
|
||||
water_heater: WaterHeaterEntityFeature,
|
||||
weather: WeatherEntityFeature,
|
||||
};
|
||||
|
||||
export function getFeatures(domain: string): FeatureEnum | undefined {
|
||||
const enumObj = DOMAIN_ENUMS[domain] as FeatureEnum;
|
||||
return enumObj;
|
||||
}
|
||||
@@ -22,16 +22,13 @@ export const FIXED_DOMAIN_STATES = {
|
||||
assist_satellite: ["idle", "listening", "responding", "processing"],
|
||||
automation: ["on", "off"],
|
||||
binary_sensor: ["on", "off"],
|
||||
button: [],
|
||||
calendar: ["on", "off"],
|
||||
camera: ["idle", "recording", "streaming"],
|
||||
cover: ["closed", "closing", "open", "opening"],
|
||||
device_tracker: ["home", "not_home"],
|
||||
fan: ["on", "off"],
|
||||
humidifier: ["on", "off"],
|
||||
infrared: [],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: [
|
||||
@@ -56,7 +53,6 @@ export const FIXED_DOMAIN_STATES = {
|
||||
plant: ["ok", "problem"],
|
||||
radio_frequency: [],
|
||||
remote: ["on", "off"],
|
||||
scene: [],
|
||||
schedule: ["on", "off"],
|
||||
script: ["on", "off"],
|
||||
siren: ["on", "off"],
|
||||
@@ -290,6 +286,81 @@ export const getStatesDomain = (
|
||||
return result;
|
||||
};
|
||||
|
||||
// Maps a value attribute (or the main state, keyed `_`) to the attribute listing
|
||||
// its options. Naming is irregular per domain, so it's mapped explicitly.
|
||||
export const DOMAIN_OPTIONS_ATTRIBUTES: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = {
|
||||
climate: {
|
||||
_: "hvac_modes",
|
||||
fan_mode: "fan_modes",
|
||||
preset_mode: "preset_modes",
|
||||
swing_mode: "swing_modes",
|
||||
swing_horizontal_mode: "swing_horizontal_modes",
|
||||
},
|
||||
event: {
|
||||
event_type: "event_types",
|
||||
},
|
||||
fan: {
|
||||
preset_mode: "preset_modes",
|
||||
},
|
||||
humidifier: {
|
||||
mode: "available_modes",
|
||||
},
|
||||
input_select: {
|
||||
_: "options",
|
||||
},
|
||||
select: {
|
||||
_: "options",
|
||||
},
|
||||
light: {
|
||||
effect: "effect_list",
|
||||
color_mode: "supported_color_modes",
|
||||
},
|
||||
media_player: {
|
||||
sound_mode: "sound_mode_list",
|
||||
source: "source_list",
|
||||
},
|
||||
remote: {
|
||||
current_activity: "activity_list",
|
||||
},
|
||||
sensor: {
|
||||
_: "options",
|
||||
},
|
||||
vacuum: {
|
||||
fan_speed: "fan_speed_list",
|
||||
},
|
||||
water_heater: {
|
||||
_: "operation_list",
|
||||
operation_mode: "operation_list",
|
||||
},
|
||||
};
|
||||
|
||||
const DOMAIN_VALUE_ATTRIBUTES: Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
> = Object.fromEntries(
|
||||
Object.entries(DOMAIN_OPTIONS_ATTRIBUTES).map(([domain, mapping]) => [
|
||||
domain,
|
||||
Object.fromEntries(
|
||||
Object.entries(mapping).map(([value, list]) => [list, value])
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
// value attribute (or main state) → its options-list attribute
|
||||
export const getOptionsAttribute = (
|
||||
domain: string,
|
||||
attribute?: string
|
||||
): string | undefined => DOMAIN_OPTIONS_ATTRIBUTES[domain]?.[attribute ?? "_"];
|
||||
|
||||
// options-list attribute → its value attribute (`_` = main state)
|
||||
export const getValueAttribute = (
|
||||
domain: string,
|
||||
optionsAttribute: string
|
||||
): string | undefined => DOMAIN_VALUE_ATTRIBUTES[domain]?.[optionsAttribute];
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
@@ -302,78 +373,15 @@ export const getStates = (
|
||||
result.push(...getStatesDomain(hass, domain, attribute));
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
if (attribute === "event_type") {
|
||||
result.push(...state.attributes.event_types);
|
||||
}
|
||||
break;
|
||||
case "fan":
|
||||
if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
}
|
||||
break;
|
||||
case "humidifier":
|
||||
if (attribute === "mode") {
|
||||
result.push(...state.attributes.available_modes);
|
||||
}
|
||||
break;
|
||||
case "input_select":
|
||||
case "select":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.options);
|
||||
}
|
||||
break;
|
||||
case "light":
|
||||
if (attribute === "effect" && state.attributes.effect_list) {
|
||||
result.push(...state.attributes.effect_list);
|
||||
} else if (
|
||||
attribute === "color_mode" &&
|
||||
state.attributes.supported_color_modes
|
||||
) {
|
||||
result.push(...state.attributes.supported_color_modes);
|
||||
}
|
||||
break;
|
||||
case "media_player":
|
||||
if (attribute === "sound_mode") {
|
||||
result.push(...state.attributes.sound_mode_list);
|
||||
} else if (attribute === "source") {
|
||||
result.push(...state.attributes.source_list);
|
||||
}
|
||||
break;
|
||||
case "remote":
|
||||
if (attribute === "current_activity") {
|
||||
result.push(...state.attributes.activity_list);
|
||||
}
|
||||
break;
|
||||
case "sensor":
|
||||
if (!attribute && state.attributes.device_class === "enum") {
|
||||
result.push(...state.attributes.options);
|
||||
}
|
||||
break;
|
||||
case "vacuum":
|
||||
if (attribute === "fan_speed") {
|
||||
result.push(...state.attributes.fan_speed_list);
|
||||
}
|
||||
break;
|
||||
case "water_heater":
|
||||
if (!attribute || attribute === "operation_mode") {
|
||||
result.push(...state.attributes.operation_list);
|
||||
}
|
||||
break;
|
||||
const optionsAttribute = getOptionsAttribute(domain, attribute);
|
||||
if (optionsAttribute) {
|
||||
const options = state.attributes[optionsAttribute];
|
||||
// Sensors only expose their options when their device class is `enum`.
|
||||
const enumSensor =
|
||||
domain !== "sensor" || state.attributes.device_class === "enum";
|
||||
if (enumSensor && Array.isArray(options)) {
|
||||
result.push(...options);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(result)];
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { OFF, UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../const";
|
||||
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (
|
||||
[
|
||||
"button",
|
||||
"event",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"radio_frequency",
|
||||
"scene",
|
||||
].includes(domain)
|
||||
) {
|
||||
if (TIMESTAMP_STATE_DOMAINS.has(domain)) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ export type LocalizeKeys =
|
||||
| `ui.common.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
| `ui.components.logbook.messages.cleared_device_classes.${string}`
|
||||
| `ui.dialogs.entity_registry.editor.${string}`
|
||||
| `ui.dialogs.more_info_control.lawn_mower.${string}`
|
||||
| `ui.dialogs.more_info_control.vacuum.${string}`
|
||||
|
||||
@@ -4,10 +4,10 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
|
||||
|
||||
/**
|
||||
* Call a function with result caching per entity.
|
||||
* @param cacheKey key to store the cache on hass object
|
||||
* @param cacheKey key to namespace the cache
|
||||
* @param cacheTime time to cache the results
|
||||
* @param func function to fetch the data
|
||||
* @param hass Home Assistant object
|
||||
* @param hass Home Assistant object (or slice) the cache is keyed on
|
||||
* @param entityId entity to fetch data for
|
||||
* @param args extra arguments to pass to the function to fetch the data
|
||||
* @returns
|
||||
@@ -15,8 +15,12 @@ type ResultCache<T> = Record<string, Promise<T> | undefined>;
|
||||
export const timeCacheEntityPromiseFunc = async <T>(
|
||||
cacheKey: string,
|
||||
cacheTime: number,
|
||||
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
|
||||
hass: HomeAssistant,
|
||||
func: (
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
) => Promise<T>,
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
...args: any[]
|
||||
): Promise<T> => {
|
||||
@@ -39,11 +43,11 @@ export const timeCacheEntityPromiseFunc = async <T>(
|
||||
// When successful, set timer to clear cache
|
||||
() =>
|
||||
setTimeout(() => {
|
||||
cache![entityId] = undefined;
|
||||
cache[entityId] = undefined;
|
||||
}, cacheTime),
|
||||
// On failure, clear cache right away
|
||||
() => {
|
||||
cache![entityId] = undefined;
|
||||
cache[entityId] = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-svg-icon";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiCloseCircle,
|
||||
mdiHelpCircle,
|
||||
} from "@mdi/js";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -19,46 +26,59 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
private get _iconPath() {
|
||||
switch (this.state) {
|
||||
case "pass":
|
||||
return mdiCheckCircle;
|
||||
case "fail":
|
||||
return mdiCloseCircle;
|
||||
case "invalid":
|
||||
return mdiAlertCircle;
|
||||
default:
|
||||
return mdiHelpCircle;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
id="indicator"
|
||||
role="status"
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
<div id="indicator" role="status" tabindex="0" aria-label=${this.label}>
|
||||
<ha-svg-icon .path=${this._iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
top: -8px;
|
||||
inset-inline-end: -8px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: var(--ha-border-width-md) solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
#indicator ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
color: var(--ha-color-green-60);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-orange-60);
|
||||
color: var(--ha-color-orange-60);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-red-60);
|
||||
color: var(--ha-color-red-60);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -79,9 +79,11 @@ function computeTimelineEnumColor(
|
||||
const domain = computeStateDomain(stateObj);
|
||||
const states =
|
||||
FIXED_DOMAIN_STATES[domain] ||
|
||||
(domain === "sensor" &&
|
||||
stateObj.attributes.device_class === "enum" &&
|
||||
stateObj.attributes.options) ||
|
||||
((domain === "sensor" && stateObj.attributes.device_class === "enum") ||
|
||||
domain === "select" ||
|
||||
domain === "input_select"
|
||||
? stateObj.attributes.options
|
||||
: undefined) ||
|
||||
[];
|
||||
const idx = states.indexOf(state);
|
||||
if (idx === -1) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiChevronDown,
|
||||
@@ -10,7 +11,9 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
runAssistPipeline,
|
||||
type AssistPipeline,
|
||||
@@ -18,10 +21,19 @@ import {
|
||||
type ConversationChatLogToolResultDelta,
|
||||
type PipelineRunEvent,
|
||||
} from "../data/assist_pipeline";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import { ConversationEntityFeature } from "../data/conversation";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
} from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
@@ -47,8 +59,6 @@ interface AssistMessage {
|
||||
|
||||
@customElement("ha-assist-chat")
|
||||
export class HaAssistChat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public pipeline?: AssistPipeline;
|
||||
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
@@ -71,6 +81,22 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@state() private _processing = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HomeAssistant["states"];
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: HomeAssistantConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: HomeAssistantConnection;
|
||||
|
||||
private _conversationId: string | null = null;
|
||||
|
||||
private _audioRecorder?: AudioRecorder;
|
||||
@@ -86,7 +112,7 @@ export class HaAssistChat extends LitElement {
|
||||
this._conversation = [
|
||||
{
|
||||
who: "hass",
|
||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
text: this._localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||
thinking: "",
|
||||
tool_calls: {},
|
||||
},
|
||||
@@ -124,9 +150,9 @@ export class HaAssistChat extends LitElement {
|
||||
const controlHA = !this.pipeline
|
||||
? false
|
||||
: this.pipeline.prefer_local_intents ||
|
||||
(this.hass.states[this.pipeline.conversation_engine]
|
||||
(this._states[this.pipeline.conversation_engine]
|
||||
? supportsFeature(
|
||||
this.hass.states[this.pipeline.conversation_engine],
|
||||
this._states[this.pipeline.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
)
|
||||
: true);
|
||||
@@ -139,7 +165,7 @@ export class HaAssistChat extends LitElement {
|
||||
? nothing
|
||||
: html`
|
||||
<ha-alert>
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.conversation_no_control"
|
||||
)}
|
||||
</ha-alert>
|
||||
@@ -180,7 +206,7 @@ export class HaAssistChat extends LitElement {
|
||||
.path=${mdiCommentProcessingOutline}
|
||||
></ha-svg-icon>
|
||||
<span class="thinking-label">
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.show_details"
|
||||
)}
|
||||
</span>
|
||||
@@ -251,7 +277,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
id="message-input"
|
||||
@keyup=${this._handleKeyUp}
|
||||
@input=${this._handleInput}
|
||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||
.label=${this._localize(`ui.dialogs.voice_command.input_label`)}
|
||||
>
|
||||
<div slot="end">
|
||||
${this._showSendButton || !supportsSTT
|
||||
@@ -261,7 +287,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiSend}
|
||||
@click=${this._handleSendMessage}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.send_text"
|
||||
)}
|
||||
>
|
||||
@@ -282,7 +308,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
.path=${mdiMicrophone}
|
||||
@click=${this._handleListeningButton}
|
||||
.disabled=${this._processing}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
"ui.dialogs.voice_command.start_listening"
|
||||
)}
|
||||
>
|
||||
@@ -391,21 +417,21 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this.hass.localize(
|
||||
html`${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
this._config,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
>${this._localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
@@ -443,7 +469,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event: PipelineRunEvent) => {
|
||||
if (event.type === "run-start") {
|
||||
this._stt_binary_handler_id =
|
||||
@@ -539,7 +565,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
}
|
||||
|
||||
private _sendAudioChunk(chunk: Int16Array) {
|
||||
this.hass.connection.socket!.binaryType = "arraybuffer";
|
||||
this._connection.connection.socket!.binaryType = "arraybuffer";
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this._stt_binary_handler_id == undefined) {
|
||||
@@ -550,7 +576,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
data[0] = this._stt_binary_handler_id;
|
||||
data.set(new Uint8Array(chunk.buffer), 1);
|
||||
|
||||
this.hass.connection.socket!.send(data);
|
||||
this._connection.connection.socket!.send(data);
|
||||
}
|
||||
|
||||
private _unloadAudio = () => {
|
||||
@@ -570,7 +596,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
hassMessageProcesser.addMessage();
|
||||
try {
|
||||
const unsub = await runAssistPipeline(
|
||||
this.hass,
|
||||
this._connection,
|
||||
(event) => {
|
||||
if (event.type.startsWith("intent-")) {
|
||||
hassMessageProcesser.processEvent(event);
|
||||
@@ -593,7 +619,7 @@ ${JSON.stringify(toolCall.result, null, 2)}</pre
|
||||
);
|
||||
} catch {
|
||||
hassMessageProcesser.setError(
|
||||
this.hass.localize("ui.dialogs.voice_command.error")
|
||||
this._localize("ui.dialogs.voice_command.error")
|
||||
);
|
||||
} finally {
|
||||
this._processing = false;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import { attributeIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-attribute-icon")
|
||||
export class HaAttributeIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property() public attribute?: string;
|
||||
@@ -19,6 +23,18 @@ export class HaAttributeIcon extends LitElement {
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
private _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -28,12 +44,14 @@ export class HaAttributeIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const icon = attributeIcon(
|
||||
this.hass,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this._entities,
|
||||
this.stateObj,
|
||||
this.attribute,
|
||||
this.attributeValue
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { getValueAttribute } from "../common/entity/get_states";
|
||||
import { formattersContext } from "../data/context";
|
||||
|
||||
@customElement("ha-attribute-value")
|
||||
@@ -56,6 +58,26 @@ class HaAttributeValue extends LitElement {
|
||||
return html`<pre>${until(yaml, "")}</pre>`;
|
||||
}
|
||||
|
||||
// Options-list attributes (effect_list, preset_modes, …) translated through
|
||||
// their value attribute, or the main state for lists like hvac_modes.
|
||||
if (Array.isArray(attributeValue)) {
|
||||
const domain = computeStateDomain(this.stateObj);
|
||||
const valueAttribute = getValueAttribute(domain, this.attribute);
|
||||
if (valueAttribute) {
|
||||
return attributeValue
|
||||
.map((item) =>
|
||||
valueAttribute === "_"
|
||||
? this._formatters!.formatEntityState(this.stateObj!, item)
|
||||
: this._formatters!.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
valueAttribute,
|
||||
item
|
||||
)
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hideUnit) {
|
||||
const parts = this._formatters!.formatEntityAttributeValueToParts(
|
||||
this.stateObj!,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@@ -15,17 +17,16 @@ export class HaBigNumber extends LitElement {
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render() {
|
||||
const formatted = formatNumber(
|
||||
this.value,
|
||||
this.hass?.locale,
|
||||
this.formatOptions
|
||||
);
|
||||
const locale = this._i18n!.locale;
|
||||
const formatted = formatNumber(this.value, locale, this.formatOptions);
|
||||
const [integer] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
@@ -33,9 +34,7 @@ export class HaBigNumber extends LitElement {
|
||||
const temperatureDecimal = formatted.replace(integer, "");
|
||||
|
||||
const formattedValue = `${this.value}${
|
||||
this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass?.locale)}${this.unit}`
|
||||
: ""
|
||||
this.unit ? `${blankBeforeUnit(this.unit, locale)}${this.unit}` : ""
|
||||
}`;
|
||||
|
||||
const unitBottom = this.unitPosition === "bottom";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
@@ -7,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import {
|
||||
CAMERA_SUPPORT_STREAM,
|
||||
CameraEntityFeature,
|
||||
type CameraCapabilities,
|
||||
type CameraEntity,
|
||||
computeMJPEGStreamUrl,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
STREAM_TYPE_WEB_RTC,
|
||||
type StreamType,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, configContext, connectionContext } from "../data/context";
|
||||
import "./ha-hls-player";
|
||||
import "./ha-web-rtc-player";
|
||||
|
||||
@@ -30,7 +31,17 @@ interface Stream {
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
export class HaCameraStream extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CameraEntity;
|
||||
|
||||
@@ -58,21 +69,33 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>): void {
|
||||
private _thumbnailApi = memoizeOne(
|
||||
(
|
||||
api: ContextType<typeof apiContext>,
|
||||
connection: ContextType<typeof connectionContext>
|
||||
) => ({
|
||||
callWS: api.callWS,
|
||||
hassUrl: connection.hassUrl,
|
||||
})
|
||||
);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues): void {
|
||||
const entityChanged =
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
|
||||
this.stateObj.entity_id;
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| ContextType<typeof configContext>
|
||||
| undefined;
|
||||
const backendStarted =
|
||||
changedProps.has("hass") &&
|
||||
this.hass &&
|
||||
changedProps.has("_config") &&
|
||||
this._config &&
|
||||
this.stateObj &&
|
||||
oldHass &&
|
||||
this.hass.config.state === STATE_RUNNING &&
|
||||
oldHass.config?.state !== STATE_RUNNING;
|
||||
oldConfig &&
|
||||
this._config.config.state === STATE_RUNNING &&
|
||||
oldConfig.config?.state !== STATE_RUNNING;
|
||||
|
||||
if (entityChanged || backendStarted) {
|
||||
this._getCapabilities();
|
||||
@@ -137,7 +160,6 @@ export class HaCameraStream extends LitElement {
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleHlsStreams}
|
||||
@@ -153,7 +175,6 @@ export class HaCameraStream extends LitElement {
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
.controls=${this.controls}
|
||||
.hass=${this.hass}
|
||||
.entityid=${this.stateObj.entity_id}
|
||||
.posterUrl=${this._posterUrl}
|
||||
@streams=${this._handleWebRtcStreams}
|
||||
@@ -170,12 +191,12 @@ export class HaCameraStream extends LitElement {
|
||||
this._capabilities = undefined;
|
||||
this._hlsStreams = undefined;
|
||||
this._webRtcStreams = undefined;
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
if (!supportsFeature(this.stateObj!, CameraEntityFeature.STREAM)) {
|
||||
this._capabilities = { frontend_stream_types: [] };
|
||||
return;
|
||||
}
|
||||
this._capabilities = await fetchCameraCapabilities(
|
||||
this.hass!,
|
||||
this._api,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
}
|
||||
@@ -183,7 +204,7 @@ export class HaCameraStream extends LitElement {
|
||||
private async _getPosterUrl(): Promise<void> {
|
||||
try {
|
||||
this._posterUrl = await fetchThumbnailUrlWithCache(
|
||||
this.hass!,
|
||||
this._thumbnailApi(this._api, this._connection),
|
||||
this.stateObj!.entity_id,
|
||||
this.clientWidth,
|
||||
this.clientHeight
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type HlsType from "hls.js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { fetchStreamUrl } from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, configContext, connectionContext } from "../data/context";
|
||||
import "./ha-alert";
|
||||
|
||||
type HlsLite = Omit<
|
||||
@@ -17,7 +20,21 @@ type HlsLite = Omit<
|
||||
|
||||
@customElement("ha-hls-player")
|
||||
class HaHLSPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@@ -140,7 +157,7 @@ class HaHLSPlayer extends LitElement {
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
|
||||
if (!isComponentLoaded(this.hass.config, "stream")) {
|
||||
if (!isComponentLoaded(this._config.config, "stream")) {
|
||||
this._setFatalError("Streaming component is not loaded.");
|
||||
return;
|
||||
}
|
||||
@@ -149,9 +166,12 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(this.hass!, this.entityid);
|
||||
const { url } = await fetchStreamUrl(
|
||||
{ callWS: this._api.callWS, hassUrl: this._connection.hassUrl },
|
||||
this.entityid
|
||||
);
|
||||
|
||||
this._url = this.hass.hassUrl(url);
|
||||
this._url = this._connection.hassUrl(url);
|
||||
this._cleanUp();
|
||||
this._resetError();
|
||||
this._startHls();
|
||||
@@ -184,13 +204,13 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
if (!hlsSupported) {
|
||||
this._setFatalError(
|
||||
this.hass.localize("ui.components.media-browser.video_not_supported")
|
||||
this._localize("ui.components.media-browser.video_not_supported")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const useExoPlayer =
|
||||
this.allowExoPlayer && this.hass.auth.external?.config.hasExoPlayer;
|
||||
this.allowExoPlayer && this._config.auth.external?.config.hasExoPlayer;
|
||||
const masterPlaylist = await (await masterPlaylistPromise).text();
|
||||
|
||||
if (!this.isConnected) {
|
||||
@@ -236,7 +256,7 @@ class HaHLSPlayer extends LitElement {
|
||||
window.addEventListener("resize", this._resizeExoPlayer);
|
||||
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
|
||||
this._videoEl.style.visibility = "hidden";
|
||||
await this.hass!.auth.external!.fireMessage({
|
||||
await this._config.auth.external!.fireMessage({
|
||||
type: "exoplayer/play_hls",
|
||||
payload: {
|
||||
url,
|
||||
@@ -250,7 +270,7 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
this._config.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
payload: {
|
||||
left: rect.left,
|
||||
@@ -362,7 +382,7 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
if (this._exoPlayer) {
|
||||
window.removeEventListener("resize", this._resizeExoPlayer);
|
||||
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
this._config.auth.external!.fireMessage({ type: "exoplayer/stop" });
|
||||
this._exoPlayer = false;
|
||||
}
|
||||
if (this._videoEl) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { HomeAssistantInternationalization } from "../types";
|
||||
class HaRelativeTime extends ReactiveElement {
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@property() public format: Intl.RelativeTimeFormatStyle = "long";
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
@state()
|
||||
@@ -36,13 +38,15 @@ class HaRelativeTime extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<this>) {
|
||||
super.update(changedProps);
|
||||
if (changedProps.has("datetime")) {
|
||||
if (this.datetime) {
|
||||
this._startInterval();
|
||||
} else {
|
||||
this._clearInterval();
|
||||
}
|
||||
}
|
||||
this._updateRelative();
|
||||
}
|
||||
|
||||
@@ -66,15 +70,23 @@ class HaRelativeTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
|
||||
this.textContent = this._i18n.localize(
|
||||
"ui.components.relative_time.never"
|
||||
);
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this._i18n.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
const relTime = relativeTime(
|
||||
date,
|
||||
this._i18n.locale,
|
||||
undefined,
|
||||
true,
|
||||
this.format
|
||||
);
|
||||
this.textContent = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ export class HaDateTimeSelector extends LitElement {
|
||||
static styles = css`
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Align the input fields by their top edge so the date field's underline
|
||||
lines up with the time field, since ha-date-input reserves extra space
|
||||
below for its hint while ha-time-input does not. */
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-time-format-picker";
|
||||
|
||||
@customElement("ha-selector-ui_time_format")
|
||||
export class HaSelectorUiTimeFormat extends LitElement {
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-format-picker
|
||||
.label=${this.label}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-time-format-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_time_format": HaSelectorUiTimeFormat;
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ const LOAD_ELEMENTS = {
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
ui_time_format: () => import("./ha-selector-ui-time-format"),
|
||||
};
|
||||
|
||||
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "./ha-select";
|
||||
import type { TimestampRenderingFormat } from "../panels/lovelace/components/types";
|
||||
import { TIMESTAMP_RENDERING_FORMATS } from "../panels/lovelace/components/types";
|
||||
|
||||
@customElement("ha-time-format-picker")
|
||||
export class HaTimeFormatPicker extends LitElement {
|
||||
@property() public value?: TimestampRenderingFormat;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
private _options = memoizeOne((localize: LocalizeFunc) =>
|
||||
[{ label: localize("ui.common.auto"), value: "auto" }].concat(
|
||||
TIMESTAMP_RENDERING_FORMATS.map((format) => ({
|
||||
label:
|
||||
localize(`ui.components.time-format-picker.formats.${format}`) ||
|
||||
format,
|
||||
value: format,
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
private _styleOptions = memoizeOne((localize: LocalizeFunc) => [
|
||||
{ label: localize("ui.common.auto"), value: "auto" },
|
||||
{
|
||||
label: localize("ui.components.time-format-picker.styles.short"),
|
||||
value: "short",
|
||||
},
|
||||
{
|
||||
label: localize("ui.components.time-format-picker.styles.long"),
|
||||
value: "long",
|
||||
},
|
||||
]);
|
||||
|
||||
protected render() {
|
||||
const type = typeof this.value === "object" ? this.value.type : this.value;
|
||||
const style = typeof this.value === "object" ? this.value.style : undefined;
|
||||
return html`
|
||||
<div class="row">
|
||||
<ha-select
|
||||
.label=${this.label ?? ""}
|
||||
.value=${type || "auto"}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._selectChanged}
|
||||
.options=${this._options(this._localize)}
|
||||
>
|
||||
</ha-select>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this._localize(
|
||||
"ui.components.time-format-picker.style"
|
||||
)}
|
||||
.value=${style || "auto"}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._styleChanged}
|
||||
.options=${this._styleOptions(this._localize)}
|
||||
>
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _selectChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail?.value === "auto" && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.value && typeof this.value === "object" && this.value.style) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
type: ev.detail.value,
|
||||
style: this.value.style,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _styleChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const type = typeof this.value === "object" ? this.value.type : this.value;
|
||||
if (ev.detail?.value === "auto") {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
type: type,
|
||||
style: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-time-format-picker": HaTimeFormatPicker;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
webRtcOffer,
|
||||
type WebRtcOfferEvent,
|
||||
} from "../data/camera";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { apiContext, connectionContext } from "../data/context";
|
||||
import "./ha-alert";
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,13 @@ import "./ha-alert";
|
||||
*/
|
||||
@customElement("ha-web-rtc-player")
|
||||
class HaWebRtcPlayer extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: ContextType<typeof connectionContext>;
|
||||
|
||||
@property() public entityid?: string;
|
||||
|
||||
@@ -130,7 +137,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hass || !this.entityid) {
|
||||
if (!this._api || !this._connection || !this.entityid) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +148,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._logEvent("start clientConfig");
|
||||
|
||||
this._clientConfig = await fetchWebRtcClientConfiguration(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid
|
||||
);
|
||||
|
||||
@@ -230,8 +237,11 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._logEvent("start webRtcOffer", offer_sdp);
|
||||
|
||||
try {
|
||||
this._unsub = webRtcOffer(this.hass, this.entityid, offer_sdp, (event) =>
|
||||
this._handleOfferEvent(event)
|
||||
this._unsub = webRtcOffer(
|
||||
this._connection,
|
||||
this.entityid,
|
||||
offer_sdp,
|
||||
(event) => this._handleOfferEvent(event)
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = "Failed to start WebRTC stream: " + err.message;
|
||||
@@ -257,7 +267,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
this._sessionId = event.session_id;
|
||||
this._candidatesList.forEach((candidate) =>
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid!,
|
||||
event.session_id,
|
||||
// toJSON returns RTCIceCandidateInit
|
||||
@@ -310,7 +320,7 @@ class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
if (this._sessionId) {
|
||||
addWebRtcCandidate(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entityid,
|
||||
this._sessionId,
|
||||
// toJSON returns RTCIceCandidateInit
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("ha-entity-marker")
|
||||
class HaEntityMarker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "entity-id", reflect: true }) public entityId?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["entityId"] })
|
||||
private _stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
||||
|
||||
@property({ attribute: "entity-unit" }) public entityUnit?: string;
|
||||
@@ -36,9 +39,7 @@ class HaEntityMarker extends LitElement {
|
||||
})}
|
||||
></div>`
|
||||
: this.showIcon && this.entityId
|
||||
? html`<ha-state-icon
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
? html`<ha-state-icon .stateObj=${this._stateObj}></ha-state-icon>`
|
||||
: !this.entityUnit
|
||||
? this.entityName
|
||||
: html`
|
||||
|
||||
@@ -128,7 +128,6 @@ export class HaLocationsEditor extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-map
|
||||
.hass=${this.hass}
|
||||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { isToday } from "date-fns";
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
formatTimeWeekday,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
|
||||
@@ -26,7 +29,22 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getEntityLocation } from "../../common/entity/get_entity_location";
|
||||
import { DecoratedMarker } from "../../common/map/decorated_marker";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../data/context";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantUI,
|
||||
ThemeMode,
|
||||
} from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
@@ -76,7 +94,32 @@ export interface HaMapEntity {
|
||||
|
||||
@customElement("ha-map")
|
||||
export class HaMap extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HassEntities;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config!: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
private _ui!: HomeAssistantUI;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection!: HomeAssistantConnection;
|
||||
|
||||
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
|
||||
|
||||
@@ -175,17 +218,16 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
let autoFitRequired = false;
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldStates = changedProps.get("_states") as HassEntities | undefined;
|
||||
|
||||
if (changedProps.has("_loaded") || changedProps.has("entities")) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
} else if (this._loaded && oldHass && this.entities) {
|
||||
} else if (this._loaded && oldStates && this.entities) {
|
||||
// Check if any state has changed
|
||||
for (const entity of this.entities) {
|
||||
if (
|
||||
oldHass.states[getEntityId(entity)] !==
|
||||
this.hass!.states[getEntityId(entity)]
|
||||
oldStates[getEntityId(entity)] !== this._states[getEntityId(entity)]
|
||||
) {
|
||||
this._drawEntities();
|
||||
autoFitRequired = !this._pauseAutoFit;
|
||||
@@ -219,10 +261,11 @@ export class HaMap extends ReactiveElement {
|
||||
}, PROGRAMMITIC_FIT_DELAY);
|
||||
}
|
||||
|
||||
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
|
||||
if (
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
(!changedProps.has("_ui") ||
|
||||
(oldUi && oldUi.themes?.darkMode === this._ui.themes?.darkMode))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -233,7 +276,7 @@ export class HaMap extends ReactiveElement {
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
|
||||
(this.themeMode === "auto" && Boolean(this._ui?.themes.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,8 +301,8 @@ export class HaMap extends ReactiveElement {
|
||||
this._loading = true;
|
||||
try {
|
||||
[this.leafletMap, this.Leaflet] = await setupLeafletMap(map, {
|
||||
latitude: this.hass?.config.latitude ?? 52.3731339,
|
||||
longitude: this.hass?.config.longitude ?? 4.8903147,
|
||||
latitude: this._config?.latitude ?? 52.3731339,
|
||||
longitude: this._config?.longitude ?? 4.8903147,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
this._updateMapStyle();
|
||||
@@ -300,7 +343,7 @@ export class HaMap extends ReactiveElement {
|
||||
if (options?.unpause_autofit) {
|
||||
this._pauseAutoFit = false;
|
||||
}
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
if (!this.leafletMap || !this.Leaflet || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,10 +354,7 @@ export class HaMap extends ReactiveElement {
|
||||
) {
|
||||
this._isProgrammaticFit = true;
|
||||
this.leafletMap.setView(
|
||||
new this.Leaflet.LatLng(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude
|
||||
),
|
||||
new this.Leaflet.LatLng(this._config.latitude, this._config.longitude),
|
||||
options?.zoom || this.zoom
|
||||
);
|
||||
setTimeout(() => {
|
||||
@@ -351,7 +391,7 @@ export class HaMap extends ReactiveElement {
|
||||
boundingbox: LatLngExpression[],
|
||||
options?: { zoom?: number; pad?: number }
|
||||
) {
|
||||
if (!this.leafletMap || !this.Leaflet || !this.hass) {
|
||||
if (!this.leafletMap || !this.Leaflet) {
|
||||
return;
|
||||
}
|
||||
const bounds = this.Leaflet.latLngBounds(boundingbox).pad(
|
||||
@@ -382,32 +422,31 @@ export class HaMap extends ReactiveElement {
|
||||
if (path.fullDatetime) {
|
||||
formattedTime = formatDateTime(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
} else if (isToday(point.timestamp)) {
|
||||
formattedTime = formatTimeWithSeconds(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
} else {
|
||||
formattedTime = formatTimeWeekday(
|
||||
point.timestamp,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
}
|
||||
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
|
||||
}
|
||||
|
||||
private _drawPaths(): void {
|
||||
const hass = this.hass;
|
||||
const map = this.leafletMap;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Leaflet = this.Leaflet;
|
||||
|
||||
if (!hass || !map || !Leaflet) {
|
||||
if (!this._i18n || !this._config || !map || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
if (this._mapPaths.length) {
|
||||
@@ -535,12 +574,12 @@ export class HaMap extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _drawEntities(): void {
|
||||
const hass = this.hass;
|
||||
const states = this._states;
|
||||
const map = this.leafletMap;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Leaflet = this.Leaflet;
|
||||
|
||||
if (!hass || !map || !Leaflet) {
|
||||
if (!states || !map || !Leaflet) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,7 +617,7 @@ export class HaMap extends ReactiveElement {
|
||||
const className = this._darkMode ? "dark" : "light";
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = hass.states[getEntityId(entity)];
|
||||
const stateObj = states[getEntityId(entity)];
|
||||
if (!stateObj) {
|
||||
continue;
|
||||
}
|
||||
@@ -591,7 +630,7 @@ export class HaMap extends ReactiveElement {
|
||||
entity_picture: entityPicture,
|
||||
} = stateObj.attributes;
|
||||
|
||||
const location = getEntityLocation(stateObj, hass.states);
|
||||
const location = getEntityLocation(stateObj, states);
|
||||
if (!location) {
|
||||
continue;
|
||||
}
|
||||
@@ -648,11 +687,14 @@ export class HaMap extends ReactiveElement {
|
||||
// create icon
|
||||
const entityName =
|
||||
typeof entity !== "string" && entity.label_mode === "state"
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
? this._formatters.formatEntityState(stateObj)
|
||||
: typeof entity !== "string" &&
|
||||
entity.label_mode === "attribute" &&
|
||||
entity.attribute !== undefined
|
||||
? this.hass.formatEntityAttributeValue(stateObj, entity.attribute)
|
||||
? this._formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
entity.attribute
|
||||
)
|
||||
: (customTitle ??
|
||||
title
|
||||
.split(" ")
|
||||
@@ -661,7 +703,6 @@ export class HaMap extends ReactiveElement {
|
||||
.substr(0, 3));
|
||||
|
||||
const entityMarker = document.createElement("ha-entity-marker");
|
||||
entityMarker.hass = this.hass;
|
||||
entityMarker.showIcon =
|
||||
typeof entity !== "string" && entity.label_mode === "icon";
|
||||
entityMarker.entityId = getEntityId(entity);
|
||||
@@ -674,7 +715,7 @@ export class HaMap extends ReactiveElement {
|
||||
: "";
|
||||
entityMarker.entityPicture =
|
||||
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
||||
? this.hass.hassUrl(entityPicture)
|
||||
? this._connection.hassUrl(entityPicture)
|
||||
: "";
|
||||
if (typeof entity !== "string") {
|
||||
entityMarker.entityColor = entity.color;
|
||||
|
||||
@@ -26,7 +26,6 @@ export class HaTraceLogbook extends LitElement {
|
||||
return this.logbookEntries.length
|
||||
? html`
|
||||
<ha-logbook-renderer
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
|
||||
@@ -388,7 +388,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
return entries.length
|
||||
? html`
|
||||
<ha-logbook-renderer
|
||||
relative-time
|
||||
.hass=${this.hass}
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import { localizeTriggerDescription } from "../../data/logbook";
|
||||
import { localizeTriggerSource } from "../../data/logbook";
|
||||
import type {
|
||||
ChooseAction,
|
||||
IfAction,
|
||||
@@ -333,7 +333,7 @@ class ActionRenderer {
|
||||
: "other",
|
||||
alias: triggerStep.changed_variables.trigger?.alias,
|
||||
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
|
||||
trigger: localizeTriggerDescription(
|
||||
trigger: localizeTriggerSource(
|
||||
this.hass.localize,
|
||||
this.trace.trigger
|
||||
),
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Selector } from "./selector";
|
||||
|
||||
export const enum AITaskEntityFeature {
|
||||
export enum AITaskEntityFeature {
|
||||
GENERATE_DATA = 1,
|
||||
SUPPORT_ATTACHMENTS = 2,
|
||||
GENERATE_IMAGE = 4,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
|
||||
export const enum AlarmControlPanelEntityFeature {
|
||||
export enum AlarmControlPanelEntityFeature {
|
||||
ARM_HOME = 1,
|
||||
ARM_AWAY = 2,
|
||||
ARM_NIGHT = 4,
|
||||
@@ -108,7 +108,7 @@ export const supportedAlarmModes = (stateObj: AlarmControlPanelEntity) =>
|
||||
|
||||
export const setProtectedAlarmControlPanelMode = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService" | "localize" | "callWS">,
|
||||
stateObj: AlarmControlPanelEntity,
|
||||
mode: AlarmMode
|
||||
) => {
|
||||
|
||||
@@ -338,7 +338,7 @@ export const runDebugAssistPipeline = (
|
||||
};
|
||||
|
||||
export const runAssistPipeline = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "connection">,
|
||||
callback: (event: PipelineRunEvent) => void,
|
||||
options: PipelineRunOptions
|
||||
) =>
|
||||
@@ -379,7 +379,10 @@ export const listAssistPipelines = (hass: HomeAssistant) =>
|
||||
type: "assist_pipeline/pipeline/list",
|
||||
});
|
||||
|
||||
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
|
||||
export const getAssistPipeline = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
pipeline_id?: string
|
||||
) =>
|
||||
hass.callWS<AssistPipeline>({
|
||||
type: "assist_pipeline/pipeline/get",
|
||||
pipeline_id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum AssistSatelliteEntityFeature {
|
||||
export enum AssistSatelliteEntityFeature {
|
||||
ANNOUNCE = 1,
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const autocompleteLoginFields = (schema: HaFormSchema[]) =>
|
||||
});
|
||||
|
||||
export const getSignedPath = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
path: string
|
||||
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ export const expandConditionWithShorthand = (
|
||||
};
|
||||
|
||||
export const triggerAutomationActions = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService">,
|
||||
entityId: string
|
||||
) => {
|
||||
hass.callService("automation", "trigger", {
|
||||
|
||||
@@ -54,7 +54,7 @@ export enum RecurrenceRange {
|
||||
THISANDFUTURE = "THISANDFUTURE",
|
||||
}
|
||||
|
||||
export const enum CalendarEntityFeature {
|
||||
export enum CalendarEntityFeature {
|
||||
CREATE_EVENT = 1,
|
||||
DELETE_EVENT = 2,
|
||||
UPDATE_EVENT = 4,
|
||||
|
||||
+12
-9
@@ -7,14 +7,17 @@ import type { HomeAssistant } from "../types";
|
||||
import { getSignedPath } from "./auth";
|
||||
|
||||
export const CAMERA_ORIENTATIONS = [1, 2, 3, 4, 6, 8];
|
||||
export const CAMERA_SUPPORT_ON_OFF = 1;
|
||||
export const CAMERA_SUPPORT_STREAM = 2;
|
||||
|
||||
export const STREAM_TYPE_HLS = "hls";
|
||||
export const STREAM_TYPE_WEB_RTC = "web_rtc";
|
||||
|
||||
export type StreamType = typeof STREAM_TYPE_HLS | typeof STREAM_TYPE_WEB_RTC;
|
||||
|
||||
export enum CameraEntityFeature {
|
||||
ON_OFF = 1,
|
||||
STREAM = 2,
|
||||
}
|
||||
|
||||
interface CameraEntityAttributes extends HassEntityAttributeBase {
|
||||
model_name: string;
|
||||
access_token?: string;
|
||||
@@ -86,7 +89,7 @@ export const computeMJPEGStreamUrl = (
|
||||
: undefined;
|
||||
|
||||
export const fetchThumbnailUrlWithCache = async (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
width: number,
|
||||
height: number
|
||||
@@ -102,7 +105,7 @@ export const fetchThumbnailUrlWithCache = async (
|
||||
};
|
||||
|
||||
export const fetchThumbnailUrl = async (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string
|
||||
) => {
|
||||
const path = await getSignedPath(hass, `/api/camera_proxy/${entityId}`);
|
||||
@@ -110,7 +113,7 @@ export const fetchThumbnailUrl = async (
|
||||
};
|
||||
|
||||
export const fetchStreamUrl = async (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS" | "hassUrl">,
|
||||
entityId: string,
|
||||
format?: "hls"
|
||||
) => {
|
||||
@@ -128,7 +131,7 @@ export const fetchStreamUrl = async (
|
||||
};
|
||||
|
||||
export const webRtcOffer = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "connection">,
|
||||
entity_id: string,
|
||||
offer: string,
|
||||
callback: (event: WebRtcOfferEvent) => void
|
||||
@@ -140,7 +143,7 @@ export const webRtcOffer = (
|
||||
});
|
||||
|
||||
export const addWebRtcCandidate = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entity_id: string,
|
||||
session_id: string,
|
||||
candidate: RTCIceCandidateInit
|
||||
@@ -186,7 +189,7 @@ export interface CameraCapabilities {
|
||||
}
|
||||
|
||||
export const fetchCameraCapabilities = async (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entity_id: string
|
||||
) =>
|
||||
hass.callWS<CameraCapabilities>({ type: "camera/capabilities", entity_id });
|
||||
@@ -197,7 +200,7 @@ export interface WebRTCClientConfiguration {
|
||||
}
|
||||
|
||||
export const fetchWebRtcClientConfiguration = async (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entityId: string
|
||||
) =>
|
||||
hass.callWS<WebRTCClientConfiguration>({
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export type ClimateEntity = HassEntityBase & {
|
||||
};
|
||||
};
|
||||
|
||||
export const enum ClimateEntityFeature {
|
||||
export enum ClimateEntityFeature {
|
||||
TARGET_TEMPERATURE = 1,
|
||||
TARGET_TEMPERATURE_RANGE = 2,
|
||||
TARGET_HUMIDITY = 4,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum ConversationEntityFeature {
|
||||
export enum ConversationEntityFeature {
|
||||
CONTROL = 1,
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -4,10 +4,10 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantFormatters } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum CoverEntityFeature {
|
||||
export enum CoverEntityFeature {
|
||||
OPEN = 1,
|
||||
CLOSE = 2,
|
||||
SET_POSITION = 4,
|
||||
@@ -122,7 +122,7 @@ export interface CoverEntity extends HassEntityBase {
|
||||
|
||||
export function computeCoverPositionStateDisplay(
|
||||
stateObj: CoverEntity,
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
position?: number
|
||||
) {
|
||||
const statePosition = stateActive(stateObj)
|
||||
@@ -133,7 +133,7 @@ export function computeCoverPositionStateDisplay(
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
||||
return currentPosition && currentPosition !== 100
|
||||
? hass.formatEntityAttributeValue(
|
||||
? formatEntityAttributeValue(
|
||||
stateObj,
|
||||
// Always use position as it's the same formatting as tilt position
|
||||
"current_position",
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
import type { HassEntityBase } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const stateToIsoDateString = (entityState: HassEntityBase) =>
|
||||
`${entityState}T00:00:00`;
|
||||
|
||||
export const setDateValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
date: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, date };
|
||||
hass.callService("date", "set_value", param);
|
||||
callService("date", "set_value", param);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const setDateTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
datetime: Date
|
||||
) => {
|
||||
hass.callService("datetime", "set_value", {
|
||||
callService("datetime", "set_value", {
|
||||
entity_id: entityId,
|
||||
datetime: datetime.toISOString(),
|
||||
});
|
||||
|
||||
@@ -211,14 +211,14 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
|
||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
||||
export const findBatteryEntity = <T extends { entity_id: string }>(
|
||||
hass: HomeAssistant,
|
||||
states: HomeAssistant["states"],
|
||||
entities: T[]
|
||||
): T | undefined => {
|
||||
const batteryEntities = entities
|
||||
.filter(
|
||||
(entity) =>
|
||||
hass.states[entity.entity_id] &&
|
||||
hass.states[entity.entity_id].attributes.device_class === "battery" &&
|
||||
states[entity.entity_id] &&
|
||||
states[entity.entity_id].attributes.device_class === "battery" &&
|
||||
batteryPriorities.includes(computeDomain(entity.entity_id))
|
||||
)
|
||||
.sort(
|
||||
@@ -234,14 +234,13 @@ export const findBatteryEntity = <T extends { entity_id: string }>(
|
||||
};
|
||||
|
||||
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
|
||||
hass: HomeAssistant,
|
||||
states: HomeAssistant["states"],
|
||||
entities: T[]
|
||||
): T | undefined =>
|
||||
entities.find(
|
||||
(entity) =>
|
||||
hass.states[entity.entity_id] &&
|
||||
hass.states[entity.entity_id].attributes.device_class ===
|
||||
"battery_charging"
|
||||
states[entity.entity_id] &&
|
||||
states[entity.entity_id].attributes.device_class === "battery_charging"
|
||||
);
|
||||
|
||||
export const computeEntityRegistryName = (
|
||||
@@ -259,7 +258,7 @@ export const computeEntityRegistryName = (
|
||||
};
|
||||
|
||||
export const getExtendedEntityRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entityId: string
|
||||
): Promise<ExtEntityRegistryEntry> =>
|
||||
hass.callWS({
|
||||
@@ -277,7 +276,7 @@ export const getExtendedEntityRegistryEntries = (
|
||||
});
|
||||
|
||||
export const updateEntityRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
entityId: string,
|
||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
): Promise<UpdateEntityRegistryEntryResult> =>
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import type {
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum FanEntityFeature {
|
||||
export enum FanEntityFeature {
|
||||
SET_SPEED = 1,
|
||||
OSCILLATE = 2,
|
||||
DIRECTION = 4,
|
||||
@@ -100,7 +100,7 @@ export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4;
|
||||
|
||||
export function computeFanSpeedStateDisplay(
|
||||
stateObj: FanEntity,
|
||||
hass: HomeAssistant,
|
||||
formatters: Pick<HomeAssistant, "formatEntityAttributeValue">,
|
||||
speed?: number
|
||||
) {
|
||||
const percentage = stateActive(stateObj)
|
||||
@@ -109,7 +109,7 @@ export function computeFanSpeedStateDisplay(
|
||||
const currentSpeed = speed ?? percentage;
|
||||
|
||||
return currentSpeed
|
||||
? hass.formatEntityAttributeValue(
|
||||
? formatters.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"percentage",
|
||||
Math.round(currentSpeed)
|
||||
|
||||
@@ -20,7 +20,7 @@ export type HumidifierEntity = HassEntityBase & {
|
||||
};
|
||||
};
|
||||
|
||||
export const enum HumidifierEntityFeature {
|
||||
export enum HumidifierEntityFeature {
|
||||
MODES = 1,
|
||||
}
|
||||
|
||||
|
||||
+10
-8
@@ -39,6 +39,7 @@ import {
|
||||
mdiMicrophoneMessage,
|
||||
mdiMotionSensor,
|
||||
mdiPalette,
|
||||
mdiRadioTower,
|
||||
mdiRayVertex,
|
||||
mdiRemote,
|
||||
mdiRobot,
|
||||
@@ -52,7 +53,6 @@ import {
|
||||
mdiThermostat,
|
||||
mdiTimerOutline,
|
||||
mdiToggleSwitch,
|
||||
mdiVideoInputAntenna,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
@@ -129,7 +129,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
plant: mdiFlower,
|
||||
power: mdiFlash,
|
||||
proximity: mdiAppleSafari,
|
||||
radio_frequency: mdiVideoInputAntenna,
|
||||
radio_frequency: mdiRadioTower,
|
||||
remote: mdiRemote,
|
||||
scene: mdiPalette,
|
||||
schedule: mdiCalendarClock,
|
||||
@@ -548,7 +548,9 @@ const getEntityIcon = async (
|
||||
};
|
||||
|
||||
export const attributeIcon = async (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: HomeAssistant["connection"],
|
||||
entities: HomeAssistant["entities"],
|
||||
state: HassEntity,
|
||||
attribute: string,
|
||||
attributeValue?: string
|
||||
@@ -556,7 +558,7 @@ export const attributeIcon = async (
|
||||
let icon: string | undefined;
|
||||
const domain = computeStateDomain(state);
|
||||
const deviceClass = state.attributes.device_class;
|
||||
const entity = hass.entities?.[state.entity_id] as
|
||||
const entity = entities[state.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
const platform = entity?.platform;
|
||||
@@ -567,8 +569,8 @@ export const attributeIcon = async (
|
||||
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
@@ -580,8 +582,8 @@ export const attributeIcon = async (
|
||||
}
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
hassConnection,
|
||||
hassConfig,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { computeDeviceName } from "../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../common/entity/compute_entity_name";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
|
||||
|
||||
// The infrared integration is an entity-type integration: its emitter and
|
||||
// receiver entities live in the `infrared` domain (their registry platform is
|
||||
// the providing integration, e.g. broadlink/esphome).
|
||||
const INFRARED_DOMAIN = "infrared";
|
||||
|
||||
export type InfraredProxyType = "emitter" | "receiver";
|
||||
|
||||
export type InfraredDeviceType = InfraredProxyType | "both";
|
||||
|
||||
export interface InfraredDevice {
|
||||
id: string;
|
||||
device_id: string | null;
|
||||
name: string;
|
||||
type: InfraredDeviceType;
|
||||
online: boolean;
|
||||
// Most recent last-used timestamp (entity state) across the device's
|
||||
// entities, as an ISO string. Undefined when never used.
|
||||
last_used?: string;
|
||||
entity_ids: string[];
|
||||
}
|
||||
|
||||
interface InfraredProxyEntity {
|
||||
entity_id: string;
|
||||
device_id: string | null;
|
||||
name: string;
|
||||
type: InfraredProxyType;
|
||||
online: boolean;
|
||||
last_used?: string;
|
||||
}
|
||||
|
||||
// Collect the infrared proxy entities from the entity registry. A proxy is an
|
||||
// entity in the `infrared` domain, classified as emitter or receiver by its
|
||||
// device class.
|
||||
const computeInfraredProxies = (
|
||||
entities: HomeAssistant["entities"],
|
||||
states: HomeAssistant["states"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): InfraredProxyEntity[] => {
|
||||
const proxies: InfraredProxyEntity[] = [];
|
||||
|
||||
for (const entry of Object.values(entities)) {
|
||||
if (computeDomain(entry.entity_id) !== INFRARED_DOMAIN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stateObj = states[entry.entity_id];
|
||||
const deviceClass = stateObj?.attributes.device_class;
|
||||
if (deviceClass !== "emitter" && deviceClass !== "receiver") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const online = stateObj.state !== UNAVAILABLE;
|
||||
|
||||
// The entity state holds the timestamp the proxy was last used (or
|
||||
// unknown/unavailable when it never has been).
|
||||
let last_used: string | undefined;
|
||||
if (stateObj.state !== UNAVAILABLE && stateObj.state !== UNKNOWN) {
|
||||
const time = new Date(stateObj.state).getTime();
|
||||
if (!isNaN(time)) {
|
||||
last_used = stateObj.state;
|
||||
}
|
||||
}
|
||||
|
||||
proxies.push({
|
||||
entity_id: entry.entity_id,
|
||||
device_id: entry.device_id ?? null,
|
||||
name: computeEntityName(stateObj, entities, devices) || entry.entity_id,
|
||||
type: deviceClass,
|
||||
online,
|
||||
last_used,
|
||||
});
|
||||
}
|
||||
|
||||
return proxies;
|
||||
};
|
||||
|
||||
// Group the proxy entities by device. A device exposing both an emitter and a
|
||||
// receiver entity is reported as type "both".
|
||||
export const computeInfraredDevices = (
|
||||
entities: HomeAssistant["entities"],
|
||||
states: HomeAssistant["states"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): InfraredDevice[] => {
|
||||
const proxies = computeInfraredProxies(entities, states, devices);
|
||||
|
||||
const groups = new Map<string, InfraredProxyEntity[]>();
|
||||
for (const proxy of proxies) {
|
||||
const key = proxy.device_id ?? `entity:${proxy.entity_id}`;
|
||||
const group = groups.get(key);
|
||||
if (group) {
|
||||
group.push(proxy);
|
||||
} else {
|
||||
groups.set(key, [proxy]);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groups.values(), (group) => {
|
||||
const hasEmitter = group.some((p) => p.type === "emitter");
|
||||
const hasReceiver = group.some((p) => p.type === "receiver");
|
||||
const type: InfraredDeviceType =
|
||||
hasEmitter && hasReceiver ? "both" : hasEmitter ? "emitter" : "receiver";
|
||||
const online = group.some((p) => p.online);
|
||||
// Across a device's entities, keep the most recent valid timestamp.
|
||||
let last_used: string | undefined;
|
||||
for (const p of group) {
|
||||
if (
|
||||
p.last_used &&
|
||||
(!last_used ||
|
||||
new Date(p.last_used).getTime() > new Date(last_used).getTime())
|
||||
) {
|
||||
last_used = p.last_used;
|
||||
}
|
||||
}
|
||||
const { device_id } = group[0];
|
||||
const device = device_id ? devices[device_id] : undefined;
|
||||
const name = (device && computeDeviceName(device)) || group[0].name;
|
||||
|
||||
return {
|
||||
id: device_id ?? group[0].entity_id,
|
||||
device_id,
|
||||
name,
|
||||
type,
|
||||
online,
|
||||
last_used,
|
||||
entity_ids: group.map((p) => p.entity_id),
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputDateTime {
|
||||
id: string;
|
||||
@@ -32,13 +32,13 @@ export const stateToIsoDateString = (entityState: HassEntity) =>
|
||||
)}`;
|
||||
|
||||
export const setInputDateTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
time: string | undefined = undefined,
|
||||
date: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, time, date };
|
||||
hass.callService("input_datetime", "set_datetime", param);
|
||||
callService("input_datetime", "set_datetime", param);
|
||||
};
|
||||
|
||||
export const fetchInputDateTime = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { HomeAssistant } from "../types";
|
||||
export const integrationsWithPanel = {
|
||||
bluetooth: "config/bluetooth",
|
||||
dhcp: "config/dhcp",
|
||||
infrared: "config/infrared",
|
||||
matter: "config/matter",
|
||||
mqtt: "config/mqtt",
|
||||
ssdp: "config/ssdp",
|
||||
|
||||
@@ -11,7 +11,7 @@ export type LawnMowerEntityState =
|
||||
| "docked"
|
||||
| "error";
|
||||
|
||||
export const enum LawnMowerEntityFeature {
|
||||
export enum LawnMowerEntityFeature {
|
||||
START_MOWING = 1,
|
||||
PAUSE = 2,
|
||||
DOCK = 4,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { temperature2rgb } from "../common/color/convert-light-color";
|
||||
|
||||
export const enum LightEntityFeature {
|
||||
export enum LightEntityFeature {
|
||||
EFFECT = 4,
|
||||
FLASH = 8,
|
||||
TRANSITION = 32,
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
import { getExtendedEntityRegistryEntry } from "./entity/entity_registry";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
export enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function canUnlock(stateObj: LockEntity) {
|
||||
|
||||
export const callProtectedLockService = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService" | "localize" | "callWS">,
|
||||
stateObj: LockEntity,
|
||||
service: ProtectedLockService
|
||||
) => {
|
||||
|
||||
+78
-195
@@ -1,15 +1,9 @@
|
||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
BINARY_STATE_OFF,
|
||||
BINARY_STATE_ON,
|
||||
DOMAINS_WITH_DYNAMIC_PICTURE,
|
||||
} from "../common/const";
|
||||
import { DOMAINS_WITH_DYNAMIC_PICTURE } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { autoCaseNoun } from "../common/translations/auto_case_noun";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
|
||||
import { isNumericEntity } from "./history";
|
||||
|
||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||
@@ -29,7 +23,7 @@ export interface LogbookEntry {
|
||||
message?: string;
|
||||
entity_id?: string;
|
||||
icon?: string;
|
||||
source?: string; // The trigger source
|
||||
source?: string; // The trigger source (English phrase, parsed for the cause)
|
||||
domain?: string;
|
||||
state?: string; // The state of the entity
|
||||
// Context data
|
||||
@@ -50,23 +44,27 @@ export interface LogbookEntry {
|
||||
// Localization mapping for all the triggers in core
|
||||
// in homeassistant.components.homeassistant.triggers
|
||||
//
|
||||
type TriggerPhraseKeys =
|
||||
| "triggered_by_numeric_state_of"
|
||||
| "triggered_by_state_of"
|
||||
| "triggered_by_event"
|
||||
| "triggered_by_time"
|
||||
| "triggered_by_time_pattern"
|
||||
| "triggered_by_homeassistant_stopping"
|
||||
| "triggered_by_homeassistant_starting";
|
||||
// Keys are the bare translation keys under `ui.components.logbook`.
|
||||
//
|
||||
type TriggerPhraseKey =
|
||||
| "numeric_state_of"
|
||||
| "state_of"
|
||||
| "event"
|
||||
| "time_pattern"
|
||||
| "time"
|
||||
| "homeassistant_stopping"
|
||||
| "homeassistant_starting";
|
||||
|
||||
const triggerPhrases: Record<TriggerPhraseKeys, string> = {
|
||||
triggered_by_numeric_state_of: "numeric state of", // number state trigger
|
||||
triggered_by_state_of: "state of", // state trigger
|
||||
triggered_by_event: "event", // event trigger
|
||||
triggered_by_time_pattern: "time pattern", // time trigger
|
||||
triggered_by_time: "time", // time trigger
|
||||
triggered_by_homeassistant_stopping: "Home Assistant stopping", // stop event
|
||||
triggered_by_homeassistant_starting: "Home Assistant starting", // start event
|
||||
// Order matters: "time pattern" must be tested before "time" because the
|
||||
// source phrase is matched with `startsWith`.
|
||||
const triggerPhrases: Record<TriggerPhraseKey, string> = {
|
||||
numeric_state_of: "numeric state of", // number state trigger
|
||||
state_of: "state of", // state trigger
|
||||
event: "event", // event trigger
|
||||
time_pattern: "time pattern", // time trigger
|
||||
time: "time", // time trigger
|
||||
homeassistant_stopping: "Home Assistant stopping", // stop event
|
||||
homeassistant_starting: "Home Assistant starting", // start event
|
||||
};
|
||||
|
||||
export const getLogbookDataForContext = async (
|
||||
@@ -158,215 +156,100 @@ export const createHistoricState = (
|
||||
state: state,
|
||||
attributes: {
|
||||
// Rebuild the historical state by copying static attributes only
|
||||
device_class: currentStateObj?.attributes.device_class,
|
||||
source_type: currentStateObj?.attributes.source_type,
|
||||
has_date: currentStateObj?.attributes.has_date,
|
||||
has_time: currentStateObj?.attributes.has_time,
|
||||
device_class: currentStateObj.attributes.device_class,
|
||||
unit_of_measurement: currentStateObj.attributes.unit_of_measurement,
|
||||
state_class: currentStateObj.attributes.state_class,
|
||||
options: currentStateObj.attributes.options,
|
||||
source_type: currentStateObj.attributes.source_type,
|
||||
has_date: currentStateObj.attributes.has_date,
|
||||
has_time: currentStateObj.attributes.has_time,
|
||||
// We do not want to use dynamic entity pictures (e.g., from media player) for the log book rendering,
|
||||
// as they would present a false state in the log (played media right now vs actual historic data).
|
||||
entity_picture_local: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||
computeDomain(currentStateObj.entity_id)
|
||||
)
|
||||
? undefined
|
||||
: currentStateObj?.attributes.entity_picture_local,
|
||||
: currentStateObj.attributes.entity_picture_local,
|
||||
entity_picture: DOMAINS_WITH_DYNAMIC_PICTURE.has(
|
||||
computeDomain(currentStateObj.entity_id)
|
||||
)
|
||||
? undefined
|
||||
: currentStateObj?.attributes.entity_picture,
|
||||
: currentStateObj.attributes.entity_picture,
|
||||
},
|
||||
}) as unknown as HassEntity;
|
||||
|
||||
// Localize a backend trigger `source` phrase (e.g. "state of sensor.x") by
|
||||
// translating the leading phrase while keeping the entity id. The automation
|
||||
// trace timeline frames it with its own "triggered by" wording, so we only
|
||||
// translate the bare description here.
|
||||
export const localizeTriggerSource = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
|
||||
const phrase = triggerPhrases[key];
|
||||
if (source.startsWith(phrase)) {
|
||||
return source.replace(
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${triggerPhraseKey}`)}`
|
||||
);
|
||||
return source.replace(phrase, localize(`ui.components.logbook.${key}`));
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
// Mapping from a phrase key to the bare-phrase translation key (without the
|
||||
// "triggered by" prefix), used by localizeTriggerDescription below.
|
||||
const triggerDescriptionKeys: Record<
|
||||
TriggerPhraseKeys,
|
||||
| "numeric_state_of"
|
||||
| "state_of"
|
||||
| "event"
|
||||
export type TriggerPlatform =
|
||||
| "state"
|
||||
| "numeric_state"
|
||||
| "time"
|
||||
| "time_pattern"
|
||||
| "homeassistant_stopping"
|
||||
| "homeassistant_starting"
|
||||
> = {
|
||||
triggered_by_numeric_state_of: "numeric_state_of",
|
||||
triggered_by_state_of: "state_of",
|
||||
triggered_by_event: "event",
|
||||
triggered_by_time_pattern: "time_pattern",
|
||||
triggered_by_time: "time",
|
||||
triggered_by_homeassistant_stopping: "homeassistant_stopping",
|
||||
triggered_by_homeassistant_starting: "homeassistant_starting",
|
||||
| "event"
|
||||
| "homeassistant";
|
||||
|
||||
// Maps the English `triggerPhrases` to automation trigger platforms, so the
|
||||
// feed can reuse the editor's trigger-type labels instead of dedicated strings.
|
||||
const triggerPlatform: Record<TriggerPhraseKey, TriggerPlatform> = {
|
||||
numeric_state_of: "numeric_state",
|
||||
state_of: "state",
|
||||
event: "event",
|
||||
time_pattern: "time_pattern",
|
||||
time: "time",
|
||||
homeassistant_stopping: "homeassistant",
|
||||
homeassistant_starting: "homeassistant",
|
||||
};
|
||||
|
||||
// Like localizeTriggerSource, but returns just the bare localized trigger
|
||||
// description (without the "triggered by" prefix). Used where the surrounding
|
||||
// template already supplies its own "triggered by" wording.
|
||||
export const localizeTriggerDescription = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
if (source.startsWith(phrase)) {
|
||||
const bareKey = triggerDescriptionKeys[triggerPhraseKey];
|
||||
return source.replace(
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${bareKey}`)}`
|
||||
);
|
||||
export interface ParsedTriggerSource {
|
||||
platform?: TriggerPlatform;
|
||||
entityId?: string;
|
||||
}
|
||||
|
||||
// Best-effort parse of the backend's English trigger `source` (e.g. "numeric
|
||||
// state of sensor.x", "time pattern") into a platform + triggering entity.
|
||||
// Temporary bridge until the backend sends the trigger structurally.
|
||||
export const parseTriggerSource = (source: string): ParsedTriggerSource => {
|
||||
for (const key of Object.keys(triggerPhrases) as TriggerPhraseKey[]) {
|
||||
const phrase = triggerPhrases[key];
|
||||
if (!source.startsWith(phrase)) {
|
||||
continue;
|
||||
}
|
||||
const rest = source.slice(phrase.length).trim();
|
||||
const entityId = /^[a-z_]+\.[a-z0-9_]+$/.test(rest) ? rest : undefined;
|
||||
return { platform: triggerPlatform[key], entityId };
|
||||
}
|
||||
return source;
|
||||
return {};
|
||||
};
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
state: string,
|
||||
stateObj: HassEntity,
|
||||
domain: string
|
||||
): string => {
|
||||
switch (domain) {
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (state === "not_home") {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_away`);
|
||||
}
|
||||
if (state === "home") {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_home`);
|
||||
}
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_at_state`, { state });
|
||||
|
||||
case "sun":
|
||||
return state === "above_horizon"
|
||||
? localize(`${LOGBOOK_LOCALIZE_PATH}.rose`)
|
||||
: localize(`${LOGBOOK_LOCALIZE_PATH}.set`);
|
||||
|
||||
case "binary_sensor": {
|
||||
const isOn = state === BINARY_STATE_ON;
|
||||
const isOff = state === BINARY_STATE_OFF;
|
||||
const device_class = stateObj.attributes.device_class;
|
||||
|
||||
if (device_class && (isOn || isOff)) {
|
||||
return (
|
||||
localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_classes" : "cleared_device_classes"}.${device_class}`,
|
||||
{
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
) || device_class,
|
||||
hass.language
|
||||
),
|
||||
}
|
||||
) ||
|
||||
// If there's no key for a specific device class, fallback to generic string
|
||||
localize(
|
||||
`${LOGBOOK_LOCALIZE_PATH}.${isOn ? "detected_device_class" : "cleared_device_class"}`,
|
||||
{
|
||||
device_class: autoCaseNoun(
|
||||
localize(
|
||||
`component.binary_sensor.entity_component.${device_class}.name`
|
||||
) || device_class,
|
||||
hass.language
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "cover":
|
||||
switch (state) {
|
||||
case "open":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
|
||||
case "opening":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
|
||||
case "closing":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
|
||||
case "closed":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "event": {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
|
||||
|
||||
// TODO: This is not working yet, as we don't get historic attribute values
|
||||
|
||||
const event_type = hass
|
||||
.formatEntityAttributeValue(stateObj, "event_type")
|
||||
?.toString();
|
||||
|
||||
if (!event_type) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`);
|
||||
}
|
||||
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event`, {
|
||||
event_type: autoCaseNoun(event_type, hass.language),
|
||||
});
|
||||
}
|
||||
|
||||
case "lock":
|
||||
switch (state) {
|
||||
case "unlocked":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_unlocked`);
|
||||
case "locking":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
|
||||
case "unlocking":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
|
||||
case "opening":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
|
||||
case "open":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opened`);
|
||||
case "locked":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
|
||||
case "jammed":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_jammed`);
|
||||
}
|
||||
break;
|
||||
// Events expose a timestamp as their state, which has no meaningful display
|
||||
// value, so keep a dedicated phrase.
|
||||
if (domain === "event") {
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.detected_event_no_type`);
|
||||
}
|
||||
|
||||
if (state === BINARY_STATE_ON) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_on`);
|
||||
}
|
||||
|
||||
if (state === BINARY_STATE_OFF) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.turned_off`);
|
||||
}
|
||||
|
||||
if (state === UNKNOWN) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unknown`);
|
||||
}
|
||||
|
||||
if (state === UNAVAILABLE) {
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.became_unavailable`);
|
||||
}
|
||||
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, {
|
||||
state: stateObj ? hass.formatEntityState(stateObj, state) : state,
|
||||
});
|
||||
// Every other domain reuses the backend state translation, so the logbook
|
||||
// speaks the same vocabulary as the rest of the UI.
|
||||
return hass.formatEntityState(stateObj, state);
|
||||
};
|
||||
|
||||
export const filterLogbookCompatibleEntities = (entity) => {
|
||||
|
||||
@@ -82,7 +82,7 @@ export interface MediaPlayerEntity extends HassEntityBase {
|
||||
| "buffering";
|
||||
}
|
||||
|
||||
export const enum MediaPlayerEntityFeature {
|
||||
export enum MediaPlayerEntityFeature {
|
||||
PAUSE = 1,
|
||||
SEEK = 2,
|
||||
VOLUME_SET = 4,
|
||||
@@ -481,7 +481,7 @@ export const setMediaPlayerVolume = (
|
||||
hass.callService("media_player", "volume_set", { entity_id, volume_level });
|
||||
|
||||
export const handleMediaControlClick = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService">,
|
||||
stateObj: MediaPlayerEntity,
|
||||
action: string
|
||||
) =>
|
||||
@@ -509,7 +509,7 @@ export const handleMediaControlClick = (
|
||||
);
|
||||
|
||||
export const mediaPlayerPlayMedia = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callService">,
|
||||
entity_id: string,
|
||||
media_content_id: string,
|
||||
media_content_type: string,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum NotifyEntityFeature {
|
||||
TITLE = 1,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const DOMAIN = "radio_frequency";
|
||||
|
||||
export interface RadioFrequencyTransmitter {
|
||||
entity_id: string;
|
||||
device_id: string | null;
|
||||
config_entry_id: string | null;
|
||||
supported_frequency_ranges: [number, number][];
|
||||
supported_modulations: string[];
|
||||
}
|
||||
|
||||
interface RadioFrequencyTransmitterList {
|
||||
transmitters: RadioFrequencyTransmitter[];
|
||||
}
|
||||
|
||||
export const fetchRadioFrequencyTransmitters = (
|
||||
hass: HomeAssistant
|
||||
): Promise<RadioFrequencyTransmitterList> =>
|
||||
hass.callWS({
|
||||
type: "radio_frequency/list",
|
||||
});
|
||||
+5
-3
@@ -3,9 +3,11 @@ import type {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const REMOTE_SUPPORT_LEARN_COMMAND = 1;
|
||||
export const REMOTE_SUPPORT_DELETE_COMMAND = 2;
|
||||
export const REMOTE_SUPPORT_ACTIVITY = 4;
|
||||
export enum RemoteEntityFeature {
|
||||
LEARN_COMMAND = 1,
|
||||
DELETE_COMMAND = 2,
|
||||
ACTIVITY = 4,
|
||||
}
|
||||
|
||||
export type RemoteEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
|
||||
+28
-7
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { Describe } from "superstruct";
|
||||
@@ -104,6 +105,9 @@ export interface Field {
|
||||
selector?: any;
|
||||
}
|
||||
|
||||
const getScriptFields = (services: HassServices, entityId: string) =>
|
||||
services.script[computeObjectId(entityId)]?.fields;
|
||||
|
||||
interface BaseAction {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
@@ -391,31 +395,41 @@ export const getActionType = (action: Action): ActionType => {
|
||||
export const isAction = (value: unknown): value is Action =>
|
||||
getActionType(value as Action) !== "unknown";
|
||||
|
||||
export const hasScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
export const hasScriptFieldsForServices = (
|
||||
services: HassServices,
|
||||
entityId: string
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
return fields !== undefined && Object.keys(fields).length > 0;
|
||||
};
|
||||
|
||||
export const hasRequiredScriptFields = (
|
||||
export const hasScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean => hasScriptFieldsForServices(hass.services, entityId);
|
||||
|
||||
export const hasRequiredScriptFieldsForServices = (
|
||||
services: HassServices,
|
||||
entityId: string
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
return (
|
||||
fields !== undefined &&
|
||||
Object.values(fields).some((field) => field.required)
|
||||
);
|
||||
};
|
||||
|
||||
export const requiredScriptFieldsFilled = (
|
||||
export const hasRequiredScriptFields = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): boolean => hasRequiredScriptFieldsForServices(hass.services, entityId);
|
||||
|
||||
export const requiredScriptFieldsFilledForServices = (
|
||||
services: HassServices,
|
||||
entityId: string,
|
||||
data?: Record<string, any>
|
||||
): boolean => {
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
const fields = getScriptFields(services, entityId);
|
||||
if (fields === undefined || Object.keys(fields).length === 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -430,6 +444,13 @@ export const requiredScriptFieldsFilled = (
|
||||
});
|
||||
};
|
||||
|
||||
export const requiredScriptFieldsFilled = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
data?: Record<string, any>
|
||||
): boolean =>
|
||||
requiredScriptFieldsFilledForServices(hass.services, entityId, data);
|
||||
|
||||
export const migrateAutomationAction = (
|
||||
action: Action | Action[]
|
||||
): Action | Action[] => {
|
||||
|
||||
@@ -82,6 +82,7 @@ export type Selector =
|
||||
| UiActionSelector
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector
|
||||
| UiTimeFormatSelector
|
||||
| BackupLocationSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
@@ -601,6 +602,10 @@ export interface UiStateContentSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiTimeFormatSelector {
|
||||
ui_time_format: {} | null;
|
||||
}
|
||||
|
||||
export interface EntityNameSelector {
|
||||
entity_name: {
|
||||
entity_id?: string;
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
export const SirenEntityFeature = {
|
||||
TURN_ON: 1,
|
||||
TURN_OFF: 2,
|
||||
TONES: 4,
|
||||
VOLUME_SET: 8,
|
||||
DURATION: 16,
|
||||
};
|
||||
export enum SirenEntityFeature {
|
||||
TURN_ON = 1,
|
||||
TURN_OFF = 2,
|
||||
TONES = 4,
|
||||
VOLUME_SET = 8,
|
||||
DURATION = 16,
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantApi } from "../types";
|
||||
|
||||
export const setTimeValue = (
|
||||
hass: HomeAssistant,
|
||||
callService: HomeAssistantApi["callService"],
|
||||
entityId: string,
|
||||
time: string | undefined = undefined
|
||||
) => {
|
||||
const param = { entity_id: entityId, time: time };
|
||||
hass.callService("time", "set_value", param);
|
||||
callService("time", "set_value", param);
|
||||
};
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ export interface TodoItem {
|
||||
completed?: string | null;
|
||||
}
|
||||
|
||||
export const enum TodoListEntityFeature {
|
||||
export enum TodoListEntityFeature {
|
||||
CREATE_TODO_ITEM = 1,
|
||||
DELETE_TODO_ITEM = 2,
|
||||
UPDATE_TODO_ITEM = 4,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export type VacuumEntityState =
|
||||
| "returning"
|
||||
| "error";
|
||||
|
||||
export const enum VacuumEntityFeature {
|
||||
export enum VacuumEntityFeature {
|
||||
TURN_ON = 1,
|
||||
TURN_OFF = 2,
|
||||
PAUSE = 4,
|
||||
|
||||
+4
-4
@@ -4,10 +4,10 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { stateActive } from "../common/entity/state_active";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistantFormatters } from "../types";
|
||||
import { UNAVAILABLE } from "./entity/entity";
|
||||
|
||||
export const enum ValveEntityFeature {
|
||||
export enum ValveEntityFeature {
|
||||
OPEN = 1,
|
||||
CLOSE = 2,
|
||||
SET_POSITION = 4,
|
||||
@@ -78,7 +78,7 @@ export interface ValveEntity extends HassEntityBase {
|
||||
|
||||
export function computeValvePositionStateDisplay(
|
||||
stateObj: ValveEntity,
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
position?: number
|
||||
) {
|
||||
const statePosition = stateActive(stateObj)
|
||||
@@ -88,7 +88,7 @@ export function computeValvePositionStateDisplay(
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
||||
return currentPosition && currentPosition !== 100
|
||||
? hass.formatEntityAttributeValue(
|
||||
? formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"current_position",
|
||||
Math.round(currentPosition)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
export const enum WaterHeaterEntityFeature {
|
||||
export enum WaterHeaterEntityFeature {
|
||||
TARGET_TEMPERATURE = 1,
|
||||
OPERATION_MODE = 2,
|
||||
AWAY_MODE = 4,
|
||||
|
||||
+13
-11
@@ -38,10 +38,11 @@ import {
|
||||
} from "../common/const";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { round } from "../common/number/round";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantFormatters } from "../types";
|
||||
|
||||
export const enum WeatherEntityFeature {
|
||||
export enum WeatherEntityFeature {
|
||||
FORECAST_DAILY = 1,
|
||||
FORECAST_HOURLY = 2,
|
||||
FORECAST_TWICE_DAILY = 4,
|
||||
@@ -220,19 +221,20 @@ const getWindBearing = (bearing: number | string): string => {
|
||||
};
|
||||
|
||||
export const getWind = (
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
localize: LocalizeFunc,
|
||||
stateObj: WeatherEntity,
|
||||
speed?: number,
|
||||
bearing?: number | string
|
||||
): string => {
|
||||
const speedText =
|
||||
speed !== undefined && speed !== null
|
||||
? hass.formatEntityAttributeValue(stateObj, "wind_speed", speed)
|
||||
? formatEntityAttributeValue(stateObj, "wind_speed", speed)
|
||||
: "-";
|
||||
if (bearing !== undefined && bearing !== null) {
|
||||
const cardinalDirection = getWindBearing(bearing);
|
||||
return `${speedText} (${
|
||||
hass.localize(
|
||||
localize(
|
||||
`ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
|
||||
) || cardinalDirection
|
||||
})`;
|
||||
@@ -278,13 +280,13 @@ export const getWeatherUnit = (
|
||||
};
|
||||
|
||||
export const getSecondaryWeatherAttribute = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "formatEntityAttributeValue" | "localize">,
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[],
|
||||
temperatureFractionDigits?: number
|
||||
): TemplateResult | undefined => {
|
||||
const extrema = getWeatherExtrema(
|
||||
hass,
|
||||
hass.formatEntityAttributeValue,
|
||||
stateObj,
|
||||
forecast,
|
||||
temperatureFractionDigits
|
||||
@@ -320,13 +322,13 @@ export const getSecondaryWeatherAttribute = (
|
||||
? html`
|
||||
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
|
||||
`
|
||||
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
|
||||
: hass.localize(`ui.card.weather.attributes.${attribute}`)}
|
||||
${hass.formatEntityAttributeValue(stateObj, attribute, roundedValue)}
|
||||
`;
|
||||
};
|
||||
|
||||
const getWeatherExtrema = (
|
||||
hass: HomeAssistant,
|
||||
formatEntityAttributeValue: HomeAssistantFormatters["formatEntityAttributeValue"],
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[],
|
||||
temperatureFractionDigits?: number
|
||||
@@ -369,11 +371,11 @@ const getWeatherExtrema = (
|
||||
|
||||
return html`
|
||||
${tempHigh
|
||||
? hass.formatEntityAttributeValue(stateObj, "temperature", tempHigh)
|
||||
? formatEntityAttributeValue(stateObj, "temperature", tempHigh)
|
||||
: ""}
|
||||
${tempLow && tempHigh ? " / " : ""}
|
||||
${tempLow
|
||||
? hass.formatEntityAttributeValue(stateObj, "temperature", tempLow)
|
||||
? formatEntityAttributeValue(stateObj, "temperature", tempLow)
|
||||
: ""}
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-control-button";
|
||||
import { apiContext, configContext } from "../../../../data/context";
|
||||
import type { CoverEntity } from "../../../../data/cover";
|
||||
import {
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS,
|
||||
@@ -20,7 +25,11 @@ import type {
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
} from "../../../../types";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
@@ -46,7 +55,20 @@ const favoriteKindFromEvent = (ev: Event): FavoriteKind =>
|
||||
|
||||
@customElement("ha-more-info-cover-favorite-positions")
|
||||
export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
|
||||
transformer: ({ user }) => user,
|
||||
})
|
||||
private _user!: HomeAssistant["user"];
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@@ -85,7 +107,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
key: FavoriteLocalizeKey,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
`ui.dialogs.more_info_control.cover.${kind === "position" ? "favorite_position" : "favorite_tilt_position"}.${key}`,
|
||||
values
|
||||
);
|
||||
@@ -124,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "cover",
|
||||
@@ -169,14 +191,14 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
}
|
||||
|
||||
if (kind === "position") {
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
this._api.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: favorite,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
this._api.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
tilt_position: favorite,
|
||||
});
|
||||
@@ -191,7 +213,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
kind,
|
||||
value === undefined ? "add_title" : "edit_title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
inputLabel: this._localize(
|
||||
kind === "position"
|
||||
? "ui.card.cover.position"
|
||||
: "ui.card.cover.tilt_position"
|
||||
@@ -311,7 +333,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
if (action === "hold" && this._user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
@@ -376,10 +398,10 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
.deleteLabel=${this._deleteLabel(kind)}
|
||||
.editMode=${this.editMode ?? false}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.isAdmin=${Boolean(this._user?.is_admin)}
|
||||
.showDone=${showDone}
|
||||
.addLabel=${this._localizeFavorite(kind, "add")}
|
||||
.doneLabel=${this.hass.localize(
|
||||
.doneLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
@@ -415,7 +437,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
${supportsPosition
|
||||
? this._renderKindSection(
|
||||
"position",
|
||||
this.hass.localize("ui.card.cover.position"),
|
||||
this._localize("ui.card.cover.position"),
|
||||
this._favoritePositions,
|
||||
showDoneOnPosition,
|
||||
showLabels
|
||||
@@ -424,7 +446,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
${supportsTiltPosition
|
||||
? this._renderKindSection(
|
||||
"tilt",
|
||||
this.hass.localize("ui.card.cover.tilt_position"),
|
||||
this._localize("ui.card.cover.tilt_position"),
|
||||
this._favoriteTiltPositions,
|
||||
true,
|
||||
showLabels
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-absolute-time";
|
||||
import "../../../components/ha-relative-time";
|
||||
import type { HomeAssistantFormatters } from "../../../types";
|
||||
import { formattersContext } from "../../../data/context";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import type { LightEntity } from "../../../data/light";
|
||||
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
|
||||
import "../../../panels/lovelace/components/hui-timestamp-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("ha-more-info-state-header")
|
||||
export class HaMoreInfoStateHeader extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LightEntity;
|
||||
|
||||
@property({ attribute: false }) public stateOverride?: string;
|
||||
@@ -21,6 +21,10 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
|
||||
@state() private _absoluteTime = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters!: HomeAssistantFormatters;
|
||||
|
||||
private _localizeState(): TemplateResult | string {
|
||||
if (
|
||||
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
|
||||
@@ -29,7 +33,6 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<hui-timestamp-display
|
||||
.hass=${this.hass}
|
||||
.ts=${new Date(this.stateObj.state)}
|
||||
format="relative"
|
||||
capitalize
|
||||
@@ -37,7 +40,7 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
return this.hass.formatEntityState(this.stateObj);
|
||||
return this._formatters?.formatEntityState(this.stateObj) ?? "";
|
||||
}
|
||||
|
||||
private _toggleAbsolute() {
|
||||
|
||||
@@ -192,7 +192,6 @@ class DialogLightColorFavorite extends DirtyStateProviderMixin<LightColorFavorit
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
@@ -202,7 +201,6 @@ class DialogLightColorFavorite extends DirtyStateProviderMixin<LightColorFavorit
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { apiContext, configContext } from "../../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../../data/entity/entity";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import { computeDefaultFavoriteColors } from "../../../../data/light";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
|
||||
import "../ha-more-info-favorites";
|
||||
import type { HaMoreInfoFavorites } from "../ha-more-info-favorites";
|
||||
@@ -23,7 +26,17 @@ declare global {
|
||||
|
||||
@customElement("ha-more-info-light-favorite-colors")
|
||||
export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config!: ContextType<typeof configContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LightEntity;
|
||||
|
||||
@@ -53,7 +66,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
private _apply(index: number): void {
|
||||
const favorite = this._favoriteColors[index];
|
||||
this.hass.callService("light", "turn_on", {
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
...favorite,
|
||||
});
|
||||
@@ -61,7 +74,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entry!.entity_id,
|
||||
{
|
||||
options_domain: "light",
|
||||
@@ -76,7 +89,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
private _add = async (): Promise<void> => {
|
||||
const color = await showLightColorFavoriteDialog(this, {
|
||||
entry: this.entry!,
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.add_title"
|
||||
),
|
||||
});
|
||||
@@ -93,7 +106,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
const color = await showLightColorFavoriteDialog(this, {
|
||||
entry: this.entry!,
|
||||
initialColor: this._favoriteColors[index],
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.edit_title"
|
||||
),
|
||||
});
|
||||
@@ -111,13 +124,13 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
private _delete = async (index: number): Promise<void> => {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
destructive: true,
|
||||
title: this.hass.localize(
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
text: this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
confirmText: this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_action"
|
||||
),
|
||||
});
|
||||
@@ -136,7 +149,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
editMode: boolean
|
||||
): TemplateResult =>
|
||||
html`<ha-favorite-color-button
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||
editMode ? "edit" : "set"
|
||||
}`,
|
||||
@@ -147,12 +160,9 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
></ha-favorite-color-button>`;
|
||||
|
||||
private _deleteLabel = (index: number): string =>
|
||||
this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete",
|
||||
{
|
||||
number: index + 1,
|
||||
}
|
||||
);
|
||||
this._localize("ui.dialogs.more_info_control.light.favorite_color.delete", {
|
||||
number: index + 1,
|
||||
});
|
||||
|
||||
private _handleFavoriteAction = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
|
||||
@@ -161,7 +171,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
if (action === "hold" && this._config.user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
@@ -210,11 +220,11 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
.deleteLabel=${this._deleteLabel as HaMoreInfoFavorites["deleteLabel"]}
|
||||
.editMode=${this.editMode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.addLabel=${this.hass.localize(
|
||||
.isAdmin=${Boolean(this._config.user?.is_admin)}
|
||||
.addLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.add"
|
||||
)}
|
||||
.doneLabel=${this.hass.localize(
|
||||
.doneLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiEyedropper } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -10,19 +11,21 @@ import {
|
||||
rgb2hs,
|
||||
rgb2hsv,
|
||||
} from "../../../../common/color/convert-color";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { throttle } from "../../../../common/util/throttle";
|
||||
import "../../../../components/ha-hs-color-picker";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-labeled-slider";
|
||||
import { apiContext } from "../../../../data/context";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import {
|
||||
getLightCurrentModeRgbColor,
|
||||
LightColorMode,
|
||||
lightSupportsColorMode,
|
||||
} from "../../../../data/light";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -32,7 +35,13 @@ declare global {
|
||||
|
||||
@customElement("light-color-rgb-picker")
|
||||
class LightRgbColorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LightEntity;
|
||||
|
||||
@@ -109,7 +118,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
${supportsRgbw || supportsRgbww
|
||||
? html`<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.color_brightness")}
|
||||
.caption=${this._localize("ui.card.light.color_brightness")}
|
||||
icon="mdi:brightness-7"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -121,7 +130,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.white_value")}
|
||||
.caption=${this._localize("ui.card.light.white_value")}
|
||||
icon="mdi:file-word-box"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -135,7 +144,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
? html`
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.cold_white_value")}
|
||||
.caption=${this._localize("ui.card.light.cold_white_value")}
|
||||
icon="mdi:file-word-box-outline"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -145,7 +154,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
></ha-labeled-slider>
|
||||
<ha-labeled-slider
|
||||
labeled
|
||||
.caption=${this.hass.localize("ui.card.light.warm_white_value")}
|
||||
.caption=${this._localize("ui.card.light.warm_white_value")}
|
||||
icon="mdi:file-word-box"
|
||||
min="0"
|
||||
max="100"
|
||||
@@ -212,10 +221,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._isInteracting ||
|
||||
(!changedProps.has("entityId") && !changedProps.has("hass"))
|
||||
) {
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,7 +352,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
private _applyColor(color: LightColor, params?: Record<string, any>) {
|
||||
fireEvent(this, "color-changed", color);
|
||||
this.hass.callService("light", "turn_on", {
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -13,11 +14,14 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { throttle } from "../../../../common/util/throttle";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import {
|
||||
apiContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../../data/entity/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import { LightColorMode } from "../../../../data/light";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -47,7 +51,13 @@ export const generateColorTemperatureGradient = (min: number, max: number) => {
|
||||
|
||||
@customElement("light-color-temp-picker")
|
||||
class LightColorTempPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: LightEntity;
|
||||
|
||||
@@ -79,7 +89,7 @@ class LightColorTempPicker extends LitElement {
|
||||
mode="cursor"
|
||||
@value-changed=${this._ctColorChanged}
|
||||
@slider-moved=${this._ctColorCursorMoved}
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.light.color_temp"
|
||||
)}
|
||||
style=${styleMap({
|
||||
@@ -88,7 +98,7 @@ class LightColorTempPicker extends LitElement {
|
||||
})}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.unit=${DOMAIN_ATTRIBUTES_UNITS.light.color_temp_kelvin}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._i18n.locale}
|
||||
>
|
||||
</ha-control-slider>
|
||||
`;
|
||||
@@ -159,7 +169,7 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
private _applyColor(color: LightColor, params?: Record<string, any>) {
|
||||
fireEvent(this, "color-changed", color);
|
||||
this.hass.callService("light", "turn_on", {
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
|
||||
@@ -65,9 +65,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
header-title=${this.hass.localize("ui.components.siren.more_controls")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-control-button";
|
||||
import { apiContext, configContext } from "../../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../../data/entity/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
|
||||
import type {
|
||||
@@ -13,7 +18,11 @@ import type {
|
||||
ValveEntityOptions,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
} from "../../../../types";
|
||||
import type { ValveEntity } from "../../../../data/valve";
|
||||
import { DEFAULT_VALVE_FAVORITE_POSITIONS } from "../../../../data/valve";
|
||||
import { normalizeFavoritePositions } from "../../../../data/favorite_positions";
|
||||
@@ -37,7 +46,20 @@ type FavoriteLocalizeKey =
|
||||
|
||||
@customElement("ha-more-info-valve-favorite-positions")
|
||||
export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HomeAssistant["user"]>({
|
||||
transformer: ({ user }) => user,
|
||||
})
|
||||
private _user!: HomeAssistant["user"];
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ValveEntity;
|
||||
|
||||
@@ -64,7 +86,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
key: FavoriteLocalizeKey,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
`ui.dialogs.more_info_control.valve.favorite_position.${key}`,
|
||||
values
|
||||
);
|
||||
@@ -88,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
currentOptions.favorite_positions = this._favoritePositions;
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "valve",
|
||||
@@ -122,7 +144,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("valve", "set_valve_position", {
|
||||
this._api.callService("valve", "set_valve_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: favorite,
|
||||
});
|
||||
@@ -135,7 +157,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
title: this._localizeFavorite(
|
||||
value === undefined ? "add_title" : "edit_title"
|
||||
),
|
||||
inputLabel: this.hass.localize("ui.card.valve.position"),
|
||||
inputLabel: this._localize("ui.card.valve.position"),
|
||||
inputType: "number",
|
||||
inputMin: "0",
|
||||
inputMax: "100",
|
||||
@@ -242,7 +264,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
if (action === "hold" && this._user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
@@ -296,10 +318,10 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
.deleteLabel=${this._deleteLabel}
|
||||
.editMode=${this.editMode ?? false}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.isAdmin=${Boolean(this._user?.is_admin)}
|
||||
.showDone=${true}
|
||||
.addLabel=${this._localizeFavorite("add")}
|
||||
.doneLabel=${this.hass.localize(
|
||||
.doneLabel=${this._localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user