mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20240731.0 (#21510)
This commit is contained in:
commit
fdf829bc81
@ -1,28 +1,25 @@
|
||||
[modern]
|
||||
# Support for dynamic import is the main litmus test for serving modern builds.
|
||||
# Although officially a ES2020 feature, browsers implemented it early, so this
|
||||
# enables all of ES2017 and some features in ES2018.
|
||||
supports es6-module-dynamic-import
|
||||
|
||||
# Exclude Safari 11-12 because of a bug in tagged template literals
|
||||
# https://bugs.webkit.org/show_bug.cgi?id=190756
|
||||
# Note: Dropping version 11 also enables several more ES2018 features
|
||||
not Safari < 13
|
||||
not iOS < 13
|
||||
|
||||
# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||
# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
|
||||
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
|
||||
# It is served to browsers meeting the following requirements:
|
||||
# - released in the last year + current alpha/beta versions
|
||||
# - Firefox extended support release (ESR)
|
||||
# - with global utilization at or above 0.5%
|
||||
# - must support dynamic import of ES modules
|
||||
# - exclude browsers no longer being maintained
|
||||
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||
unreleased versions
|
||||
last 1 year
|
||||
Firefox ESR
|
||||
>= 0.5% and supports es6-module-dynamic-import
|
||||
not dead
|
||||
not KaiOS > 0
|
||||
not QQAndroid > 0
|
||||
not UCAndroid > 0
|
||||
|
||||
# Exclude unsupported browsers
|
||||
not dead
|
||||
|
||||
[legacy]
|
||||
# Legacy builds are served when modern requirements are not met and support browsers:
|
||||
# - released in the last 7 years + current alpha/beta versionss
|
||||
# - with global utilization above 0.05%
|
||||
# - with global utilization at or above 0.05%
|
||||
# The lattermost query ensures that support for popular old browsers is not dropped too early
|
||||
# (e.g. IE 11, Android 4.4, or Samsung 4).
|
||||
#
|
||||
@ -36,4 +33,10 @@ not dead
|
||||
# As of May 2023, only web sockets must be added to the query.
|
||||
unreleased versions
|
||||
last 7 years
|
||||
> 0.05% and supports websockets
|
||||
>= 0.05% and supports websockets
|
||||
|
||||
[legacy-sw]
|
||||
# Same as legacy plus supports service workers
|
||||
unreleased versions
|
||||
last 7 years
|
||||
>= 0.05% and supports websockets and supports serviceworkers
|
||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -102,7 +102,7 @@ jobs:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.6
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@ -74,9 +74,9 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2024.07.1
|
||||
with:
|
||||
abi: cp311
|
||||
abi: cp312
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
@ -1,4 +1 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn run lint-staged --relative --shell "/bin/bash"
|
||||
|
55
.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch
Normal file
55
.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch
Normal file
@ -0,0 +1,55 @@
|
||||
diff --git a/build/inject-manifest.js b/build/inject-manifest.js
|
||||
index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644
|
||||
--- a/build/inject-manifest.js
|
||||
+++ b/build/inject-manifest.js
|
||||
@@ -104,7 +104,7 @@ async function injectManifest(config) {
|
||||
replaceString: manifestString,
|
||||
searchString: options.injectionPoint,
|
||||
});
|
||||
- filesToWrite[options.swDest] = source;
|
||||
+ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath)));
|
||||
filesToWrite[destPath] = map;
|
||||
}
|
||||
else {
|
||||
diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js
|
||||
index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644
|
||||
--- a/build/lib/translate-url-to-sourcemap-paths.js
|
||||
+++ b/build/lib/translate-url-to-sourcemap-paths.js
|
||||
@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) {
|
||||
const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url);
|
||||
if (fs_extra_1.default.existsSync(possibleSrcPath)) {
|
||||
srcPath = possibleSrcPath;
|
||||
- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url);
|
||||
+ destPath = `${swDest}.map`;
|
||||
}
|
||||
else {
|
||||
warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`;
|
||||
diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts
|
||||
index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644
|
||||
--- a/src/inject-manifest.ts
|
||||
+++ b/src/inject-manifest.ts
|
||||
@@ -129,7 +129,10 @@ export async function injectManifest(
|
||||
searchString: options.injectionPoint!,
|
||||
});
|
||||
|
||||
- filesToWrite[options.swDest] = source;
|
||||
+ filesToWrite[options.swDest] = source.replace(
|
||||
+ url!,
|
||||
+ encodeURI(upath.basename(destPath)),
|
||||
+ );
|
||||
filesToWrite[destPath] = map;
|
||||
} else {
|
||||
// If there's no sourcemap associated with swSrc, a simple string
|
||||
diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts
|
||||
index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644
|
||||
--- a/src/lib/translate-url-to-sourcemap-paths.ts
|
||||
+++ b/src/lib/translate-url-to-sourcemap-paths.ts
|
||||
@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths(
|
||||
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
|
||||
if (fse.existsSync(possibleSrcPath)) {
|
||||
srcPath = possibleSrcPath;
|
||||
- destPath = upath.resolve(upath.dirname(swDest), url);
|
||||
+ destPath = `${swDest}.map`;
|
||||
} else {
|
||||
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
|
||||
}
|
@ -47,7 +47,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||
|
||||
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
|
||||
__DEV__: !isProdBuild,
|
||||
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
|
||||
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
|
||||
__VERSION__: JSON.stringify(env.version()),
|
||||
__DEMO__: false,
|
||||
__SUPERVISOR__: false,
|
||||
@ -79,7 +79,12 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
sourceMap: !isTestBuild,
|
||||
});
|
||||
|
||||
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
module.exports.babelOptions = ({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw,
|
||||
}) => ({
|
||||
babelrc: false,
|
||||
compact: false,
|
||||
assumptions: {
|
||||
@ -87,7 +92,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
setPublicClassFields: true,
|
||||
setSpreadProperties: true,
|
||||
},
|
||||
browserslistEnv: latestBuild ? "modern" : "legacy",
|
||||
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
@ -135,8 +140,14 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
"@babel/plugin-transform-runtime",
|
||||
{ version: dependencies["@babel/runtime"] },
|
||||
],
|
||||
// Support some proposals still in TC39 process
|
||||
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
|
||||
// Transpile decorators (still in TC39 process)
|
||||
// Modern browsers support class fields and private methods, but transform is required with the older decorator version dictated by Lit
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{ version: "2018-09", decoratorsBeforeExport: true },
|
||||
],
|
||||
"@babel/plugin-transform-class-properties",
|
||||
"@babel/plugin-transform-private-methods",
|
||||
].filter(Boolean),
|
||||
exclude: [
|
||||
// \\ for Windows, / for Mac OS and Linux
|
||||
@ -215,7 +226,13 @@ module.exports.config = {
|
||||
return {
|
||||
name: "frontend" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
service_worker: "./src/entrypoints/service_worker.ts",
|
||||
"service-worker":
|
||||
!env.useRollup() && !latestBuild
|
||||
? {
|
||||
import: "./src/entrypoints/service-worker.ts",
|
||||
layer: "sw",
|
||||
}
|
||||
: "./src/entrypoints/service-worker.ts",
|
||||
app: "./src/entrypoints/app.ts",
|
||||
authorize: "./src/entrypoints/authorize.ts",
|
||||
onboarding: "./src/entrypoints/onboarding.ts",
|
||||
|
@ -1,19 +1,54 @@
|
||||
// Tasks to compress
|
||||
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import zopfli from "gulp-zopfli-green";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||
const brotliOptions = {
|
||||
skipLarger: true,
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
},
|
||||
};
|
||||
const zopfliOptions = { threshold: 150 };
|
||||
|
||||
const compressDist = (rootDir) =>
|
||||
const compressDistBrotli = (rootDir, modernDir) =>
|
||||
gulp
|
||||
.src([
|
||||
`${rootDir}/**/*.{js,json,css,svg,xml}`,
|
||||
`${rootDir}/{authorize,onboarding}.html`,
|
||||
])
|
||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
||||
base: rootDir,
|
||||
})
|
||||
.pipe(brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressDistZopfli = (rootDir, modernDir) =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
`${rootDir}/**/${filesGlob}`,
|
||||
`!${modernDir}/**/${filesGlob}`,
|
||||
`!${rootDir}/sw-modern.js`,
|
||||
`${rootDir}/{authorize,onboarding}.html`,
|
||||
],
|
||||
{ base: rootDir }
|
||||
)
|
||||
.pipe(zopfli(zopfliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
gulp.task("compress-app", () => compressDist(paths.app_output_root));
|
||||
gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));
|
||||
const compressAppBrotli = () =>
|
||||
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioBrotli = () =>
|
||||
compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest);
|
||||
|
||||
const compressAppZopfli = () =>
|
||||
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioZopfli = () =>
|
||||
compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest);
|
||||
|
||||
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import { getUserAgentRegex } from "browserslist-useragent-regexp";
|
||||
import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import { minify } from "html-minifier-terser";
|
||||
@ -17,6 +18,12 @@ const renderTemplate = (templateFile, data = {}) => {
|
||||
...data,
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
modernRegex: getUserAgentRegex({
|
||||
env: "modern",
|
||||
allowHigherVersions: true,
|
||||
mobileToDesktop: true,
|
||||
throwOnMissing: true,
|
||||
}).toString(),
|
||||
// Resolve any child/nested templates relative to the parent and pass the same data
|
||||
renderTemplate: (childTemplate) =>
|
||||
renderTemplate(
|
||||
|
@ -1,20 +1,19 @@
|
||||
// Generate service worker.
|
||||
// Based on manifest, create a file with the content as service_worker.js
|
||||
// Generate service workers
|
||||
|
||||
import fs from "fs-extra";
|
||||
import { deleteAsync } from "del";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import sourceMapUrl from "source-map-url";
|
||||
import workboxBuild from "workbox-build";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join, relative } from "node:path";
|
||||
import { injectManifest } from "workbox-build";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
|
||||
const SW_MAP = {
|
||||
[paths.app_output_latest]: "modern",
|
||||
[paths.app_output_es5]: "legacy",
|
||||
};
|
||||
|
||||
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
|
||||
|
||||
gulp.task("gen-service-worker-app-dev", (done) => {
|
||||
writeSW(
|
||||
`
|
||||
const SW_DEV =
|
||||
`
|
||||
console.debug('Service worker disabled in development');
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
@ -22,72 +21,61 @@ self.addEventListener('install', (event) => {
|
||||
// removing any prod service worker the dev might have running
|
||||
self.skipWaiting();
|
||||
});
|
||||
`
|
||||
`.trim() + "\n";
|
||||
|
||||
gulp.task("gen-service-worker-app-dev", async () => {
|
||||
await mkdir(paths.app_output_root, { recursive: true });
|
||||
await Promise.all(
|
||||
Object.values(SW_MAP).map((build) =>
|
||||
writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
)
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task("gen-service-worker-app-prod", async () => {
|
||||
// Read bundled source file
|
||||
const bundleManifestLatest = fs.readJsonSync(
|
||||
path.resolve(paths.app_output_latest, "manifest.json")
|
||||
);
|
||||
let serviceWorkerContent = fs.readFileSync(
|
||||
paths.app_output_root + bundleManifestLatest["service_worker.js"],
|
||||
"utf-8"
|
||||
);
|
||||
|
||||
// Delete old file from frontend_latest so manifest won't pick it up
|
||||
fs.removeSync(
|
||||
paths.app_output_root + bundleManifestLatest["service_worker.js"]
|
||||
);
|
||||
fs.removeSync(
|
||||
paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
|
||||
);
|
||||
|
||||
// Remove ES5
|
||||
const bundleManifestES5 = fs.readJsonSync(
|
||||
path.resolve(paths.app_output_es5, "manifest.json")
|
||||
);
|
||||
fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
|
||||
fs.removeSync(
|
||||
paths.app_output_root + bundleManifestES5["service_worker.js.map"]
|
||||
);
|
||||
|
||||
const workboxManifest = await workboxBuild.getManifest({
|
||||
// Files that mach this pattern will be considered unique and skip revision check
|
||||
// ignore JS files + translation files
|
||||
dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
|
||||
|
||||
globDirectory: paths.app_output_root,
|
||||
globPatterns: [
|
||||
"frontend_latest/*.js",
|
||||
// Cache all English translations because we catch them as fallback
|
||||
// Using pattern to match hash instead of * to avoid caching en-GB
|
||||
// 'v' added as valid hash letter because in dev we hash with 'dev'
|
||||
"static/translations/**/en-+([a-fv0-9]).json",
|
||||
// Icon shown on splash screen
|
||||
"static/icons/favicon-192x192.png",
|
||||
"static/icons/favicon.ico",
|
||||
// Common fonts
|
||||
"static/fonts/roboto/Roboto-Light.woff2",
|
||||
"static/fonts/roboto/Roboto-Medium.woff2",
|
||||
"static/fonts/roboto/Roboto-Regular.woff2",
|
||||
"static/fonts/roboto/Roboto-Bold.woff2",
|
||||
],
|
||||
});
|
||||
|
||||
for (const warning of workboxManifest.warnings) {
|
||||
console.warn(warning);
|
||||
}
|
||||
|
||||
// remove source map and add WB manifest
|
||||
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
|
||||
serviceWorkerContent = serviceWorkerContent.replace(
|
||||
"WB_MANIFEST",
|
||||
JSON.stringify(workboxManifest.manifestEntries)
|
||||
);
|
||||
|
||||
// Write new file to root
|
||||
fs.writeFileSync(swDest, serviceWorkerContent);
|
||||
});
|
||||
gulp.task("gen-service-worker-app-prod", () =>
|
||||
Promise.all(
|
||||
Object.entries(SW_MAP).map(async ([outPath, build]) => {
|
||||
const manifest = JSON.parse(
|
||||
await readFile(join(outPath, "manifest.json"), "utf-8")
|
||||
);
|
||||
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]);
|
||||
const buildDir = relative(paths.app_output_root, outPath);
|
||||
const { warnings } = await injectManifest({
|
||||
swSrc,
|
||||
swDest: join(paths.app_output_root, `sw-${build}.js`),
|
||||
injectionPoint: "__WB_MANIFEST__",
|
||||
// Files that mach this pattern will be considered unique and skip revision check
|
||||
// ignore JS files + translation files
|
||||
dontCacheBustURLsMatching: new RegExp(
|
||||
`(?:${buildDir}/.+|static/translations/.+)`
|
||||
),
|
||||
globDirectory: paths.app_output_root,
|
||||
globPatterns: [
|
||||
`${buildDir}/*.js`,
|
||||
// Cache all English translations because we catch them as fallback
|
||||
// Using pattern to match hash instead of * to avoid caching en-GB
|
||||
// 'v' added as valid hash letter because in dev we hash with 'dev'
|
||||
"static/translations/**/en-+([a-fv0-9]).json",
|
||||
// Icon shown on splash screen
|
||||
"static/icons/favicon-192x192.png",
|
||||
"static/icons/favicon.ico",
|
||||
// Common fonts
|
||||
"static/fonts/roboto/Roboto-Light.woff2",
|
||||
"static/fonts/roboto/Roboto-Medium.woff2",
|
||||
"static/fonts/roboto/Roboto-Regular.woff2",
|
||||
"static/fonts/roboto/Roboto-Bold.woff2",
|
||||
],
|
||||
globIgnores: [`${buildDir}/service-worker*`],
|
||||
});
|
||||
if (warnings.length > 0) {
|
||||
console.warn(
|
||||
`Problems while injecting ${build} service worker:\n`,
|
||||
warnings.join("\n")
|
||||
);
|
||||
}
|
||||
await deleteAsync(`${swSrc}?(.map)`);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
@ -63,14 +63,19 @@ const createWebpackConfig = ({
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$|\.ts$/,
|
||||
use: {
|
||||
use: (info) => ({
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }),
|
||||
...bundle.babelOptions({
|
||||
latestBuild,
|
||||
isProdBuild,
|
||||
isTestBuild,
|
||||
sw: info.issuerLayer === "sw",
|
||||
}),
|
||||
cacheDirectory: !isProdBuild,
|
||||
cacheCompression: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
@ -235,6 +240,7 @@ const createWebpackConfig = ({
|
||||
),
|
||||
},
|
||||
experiments: {
|
||||
layers: true,
|
||||
outputModule: true,
|
||||
},
|
||||
};
|
||||
|
@ -36,13 +36,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<script>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
<hc-layout subtitle="FAQ">
|
||||
<style>
|
||||
a {
|
||||
|
@ -13,15 +13,9 @@
|
||||
<%= renderTemplate("_social_meta.html.template") %>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<hc-connect></hc-connect>
|
||||
<script>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
|
@ -16,14 +16,8 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<cast-media-player></cast-media-player>
|
||||
<script>
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -82,6 +82,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
has_entity_name: false,
|
||||
unique_id: "co2_intensity",
|
||||
options: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
@ -100,6 +102,8 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
has_entity_name: false,
|
||||
unique_id: "grid_fossil_fuel_percentage",
|
||||
options: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -69,6 +69,14 @@
|
||||
#ha-launch-screen .ha-launch-screen-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.ohf-logo {
|
||||
color: grey;
|
||||
font-size: 12px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -79,19 +87,14 @@
|
||||
<path fill="#F2F4F9" d="m107.27 239.762-40.63-40.63c-2.09.72-4.32 1.13-6.64 1.13-11.3 0-20.5-9.2-20.5-20.5s9.2-20.5 20.5-20.5 20.5 9.2 20.5 20.5c0 2.33-.41 4.56-1.13 6.65l31.63 31.63v-115.88c-6.8-3.3395-11.5-10.3195-11.5-18.3895 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5c0 8.07-4.7 15.05-11.5 18.3895v81.27l31.46-31.46c-.62-1.96-.96-4.04-.96-6.2 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5-9.2 20.5-20.5 20.5c-2.5 0-4.88-.47-7.09-1.29L129 208.892v30.88z"/>
|
||||
</svg>
|
||||
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
|
||||
<div class="ohf-logo">
|
||||
a project from
|
||||
<img src="/static/icons/ohf.svg" alt="Open Home Foundation" height="32">
|
||||
</div>
|
||||
</div>
|
||||
<ha-demo></ha-demo>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
|
||||
<script>
|
||||
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
|
||||
if (!isS11_12) {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
import("<%= entry %>");
|
||||
<% } %>
|
||||
window.latestJS = true;
|
||||
}
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
|
BIN
gallery/public/images/paulus.jpg
Normal file
BIN
gallery/public/images/paulus.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
@ -15,6 +15,7 @@ import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@ -41,7 +42,7 @@ const ENTITIES = [
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
@ -53,6 +54,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
@ -60,6 +62,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -72,6 +76,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
@ -79,6 +84,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -91,6 +98,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
@ -98,6 +106,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -110,6 +120,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
@ -119,6 +131,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
@ -128,6 +142,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -21,6 +21,7 @@ import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@ -41,7 +42,7 @@ const ENTITIES = [
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES = [
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
{
|
||||
area_id: "bedroom",
|
||||
configuration_url: null,
|
||||
@ -53,6 +54,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "volume1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: null,
|
||||
name: "Dishwasher",
|
||||
sw_version: null,
|
||||
@ -60,6 +62,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@ -72,6 +76,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: null,
|
||||
name: "Lamp",
|
||||
sw_version: null,
|
||||
@ -79,6 +84,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@ -91,6 +98,7 @@ const DEVICES = [
|
||||
identifiers: [["demo", "pwm1"] as [string, string]],
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
model_id: null,
|
||||
name_by_user: "User name",
|
||||
name: "Technical name",
|
||||
sw_version: null,
|
||||
@ -98,6 +106,8 @@ const DEVICES = [
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -110,6 +120,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
@ -119,6 +131,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
@ -128,6 +142,8 @@ const AREAS: AreaRegistryEntry[] = [
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -138,6 +154,8 @@ const FLOORS: FloorRegistryEntry[] = [
|
||||
level: 0,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
floor_id: "first",
|
||||
@ -145,6 +163,8 @@ const FLOORS: FloorRegistryEntry[] = [
|
||||
level: 1,
|
||||
icon: "mdi:numeric-1",
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
floor_id: "second",
|
||||
@ -152,6 +172,8 @@ const FLOORS: FloorRegistryEntry[] = [
|
||||
level: 2,
|
||||
icon: "mdi:numeric-2",
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -162,6 +184,8 @@ const LABELS: LabelRegistryEntry[] = [
|
||||
icon: null,
|
||||
color: "yellow",
|
||||
description: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
{
|
||||
label_id: "entertainment",
|
||||
@ -169,6 +193,8 @@ const LABELS: LabelRegistryEntry[] = [
|
||||
icon: "mdi:popcorn",
|
||||
color: "blue",
|
||||
description: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -287,11 +287,11 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: entities
|
||||
entities:
|
||||
- type: call-service
|
||||
- type: perform-action
|
||||
icon: mdi:power
|
||||
name: Bed light
|
||||
action_name: Toggle light
|
||||
service: light.toggle
|
||||
action: light.toggle
|
||||
data:
|
||||
entity_id: light.bed_light
|
||||
- type: section
|
||||
|
3
gallery/src/pages/lovelace/picture-card.markdown
Normal file
3
gallery/src/pages/lovelace/picture-card.markdown
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Picture Card
|
||||
---
|
61
gallery/src/pages/lovelace/picture-card.ts
Normal file
61
gallery/src/pages/lovelace/picture-card.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-cards";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("person", "paulus", "home", {
|
||||
friendly_name: "Paulus",
|
||||
entity_picture: "/images/paulus.jpg",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "Image URL",
|
||||
config: `
|
||||
- type: picture
|
||||
image: /images/living_room.png
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Person entity",
|
||||
config: `
|
||||
- type: picture
|
||||
image_entity: person.paulus
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Error: Image required",
|
||||
config: `
|
||||
- type: picture
|
||||
entity: person.paulus
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-picture-card")
|
||||
class DemoPicture extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockIcons(hass);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-lovelace-picture-card": DemoPicture;
|
||||
}
|
||||
}
|
@ -25,6 +25,15 @@ const ENTITIES = [
|
||||
friendly_name: "Movement Backyard",
|
||||
device_class: "motion",
|
||||
}),
|
||||
getEntity("person", "paulus", "home", {
|
||||
friendly_name: "Paulus",
|
||||
entity_picture: "/images/paulus.jpg",
|
||||
}),
|
||||
getEntity("sensor", "battery", 35, {
|
||||
device_class: "battery",
|
||||
friendly_name: "Battery",
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@ -123,6 +132,19 @@ const CONFIGS = [
|
||||
left: 35%
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Person entity",
|
||||
config: `
|
||||
- type: picture-elements
|
||||
image_entity: person.paulus
|
||||
elements:
|
||||
- type: state-icon
|
||||
entity: sensor.battery
|
||||
style:
|
||||
top: 8%
|
||||
left: 8%
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-picture-elements-card")
|
||||
|
@ -12,6 +12,10 @@ const ENTITIES = [
|
||||
getEntity("light", "bed_light", "off", {
|
||||
friendly_name: "Bed Light",
|
||||
}),
|
||||
getEntity("person", "paulus", "home", {
|
||||
friendly_name: "Paulus",
|
||||
entity_picture: "/images/paulus.jpg",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@ -50,6 +54,13 @@ const CONFIGS = [
|
||||
entity: camera.demo_camera
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Person entity",
|
||||
config: `
|
||||
- type: picture-entity
|
||||
entity: person.paulus
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Hidden name",
|
||||
config: `
|
||||
|
@ -20,6 +20,15 @@ const ENTITIES = [
|
||||
friendly_name: "Basement Floor Wet",
|
||||
device_class: "moisture",
|
||||
}),
|
||||
getEntity("person", "paulus", "home", {
|
||||
friendly_name: "Paulus",
|
||||
entity_picture: "/images/paulus.jpg",
|
||||
}),
|
||||
getEntity("sensor", "battery", 35, {
|
||||
device_class: "battery",
|
||||
friendly_name: "Battery",
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@ -90,6 +99,15 @@ const CONFIGS = [
|
||||
- light.ceiling_lights
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Person entity",
|
||||
config: `
|
||||
- type: picture-glance
|
||||
image_entity: person.paulus
|
||||
entities:
|
||||
- sensor.battery
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Custom icon",
|
||||
config: `
|
||||
|
@ -358,13 +358,11 @@ export class DemoEntityState extends LitElement {
|
||||
},
|
||||
entity_id: {
|
||||
title: "Entity ID",
|
||||
width: "30%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
state: {
|
||||
title: "State",
|
||||
width: "20%",
|
||||
sortable: true,
|
||||
template: (entry) =>
|
||||
html`${computeStateDisplay(
|
||||
@ -379,14 +377,12 @@ export class DemoEntityState extends LitElement {
|
||||
device_class: {
|
||||
title: "Device class",
|
||||
template: (entry) => html`${entry.device_class ?? "-"}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
domain: {
|
||||
title: "Domain",
|
||||
template: (entry) => html`${computeDomain(entry.entity_id)}`,
|
||||
width: "20%",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
|
@ -203,6 +203,8 @@ const createEntityRegistryEntries = (
|
||||
options: null,
|
||||
labels: [],
|
||||
categories: {},
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@ -215,6 +217,7 @@ const createDeviceRegistryEntries = (
|
||||
connections: [],
|
||||
manufacturer: "ESPHome",
|
||||
model: "Mock Device",
|
||||
model_id: "ABC-001",
|
||||
name: "Tag Reader",
|
||||
sw_version: null,
|
||||
hw_version: "1.0.0",
|
||||
@ -227,6 +230,8 @@ const createDeviceRegistryEntries = (
|
||||
disabled_by: null,
|
||||
configuration_url: null,
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -127,14 +127,13 @@ export class HassioBackups extends LitElement {
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
flex: 2,
|
||||
template: (backup) =>
|
||||
html`${backup.name || backup.slug}
|
||||
<div class="secondary">${backup.secondary}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: this.supervisor.localize("backup.size"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
@ -142,7 +141,6 @@ export class HassioBackups extends LitElement {
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
@ -151,7 +149,6 @@ export class HassioBackups extends LitElement {
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
width: "15%",
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
|
@ -66,7 +66,8 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "5c53de3b" // The ESPHome repository
|
||||
repo.slug !== "5c53de3b" && // The ESPHome repository
|
||||
repo.slug !== "d5369777" // Music Assistant repository
|
||||
)
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
|
||||
|
@ -4,11 +4,7 @@
|
||||
el.src = src;
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
} else {
|
||||
if (<%= modernRegex %>.test(navigator.userAgent)) {
|
||||
try {
|
||||
<% for (const entry of latestEntryJS) { %>
|
||||
new Function("import('<%= entry %>')")();
|
||||
@ -17,6 +13,10 @@
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
} else {
|
||||
<% for (const entry of es5EntryJS) { %>
|
||||
loadES5("<%= entry %>");
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
67
package.json
67
package.json
@ -16,7 +16,7 @@
|
||||
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types && yarn run lint:lit",
|
||||
"format": "yarn run format:eslint && yarn run format:prettier",
|
||||
"postinstall": "husky install",
|
||||
"postinstall": "husky",
|
||||
"prepack": "pinst --disable",
|
||||
"postpack": "pinst --enable",
|
||||
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\""
|
||||
@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.7",
|
||||
"@braintree/sanitize-url": "7.0.4",
|
||||
"@babel/runtime": "7.25.0",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.17.0",
|
||||
"@codemirror/commands": "6.6.0",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.28.4",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@ -43,12 +43,12 @@
|
||||
"@formatjs/intl-numberformat": "8.10.3",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.14",
|
||||
"@fullcalendar/core": "6.1.11",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"@fullcalendar/interaction": "6.1.11",
|
||||
"@fullcalendar/list": "6.1.11",
|
||||
"@fullcalendar/luxon3": "6.1.11",
|
||||
"@fullcalendar/timegrid": "6.1.11",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@fullcalendar/list": "6.1.15",
|
||||
"@fullcalendar/luxon3": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
@ -80,7 +80,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "1.5.1",
|
||||
"@material/web": "2.0.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
@ -88,8 +88,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.1",
|
||||
"@vaadin/combo-box": "24.4.4",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.4",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -129,7 +129,7 @@
|
||||
"rrule": "2.8.1",
|
||||
"sortablejs": "1.15.2",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "1.0.4",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "2.1.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
@ -149,18 +149,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/core": "7.24.9",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-env": "7.25.0",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.4",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.6.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "21.0.0",
|
||||
"@octokit/rest": "21.0.1",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
@ -185,12 +185,13 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.57.0",
|
||||
@ -200,18 +201,19 @@
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.14.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.3",
|
||||
"eslint-plugin-unused-imports": "4.0.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.0.1",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.4.3",
|
||||
"glob": "11.0.0",
|
||||
"gulp": "5.0.0",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.1",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.0.11",
|
||||
"husky": "9.1.3",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.7",
|
||||
@ -224,27 +226,26 @@
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.3.2",
|
||||
"prettier": "3.3.3",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "18.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.4.0",
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.5.3",
|
||||
"webpack": "5.92.1",
|
||||
"typescript": "5.5.4",
|
||||
"webpack": "5.93.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
"workbox-build": "7.1.1"
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
@ -253,7 +254,7 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
|
38
public/static/icons/ohf.svg
Normal file
38
public/static/icons/ohf.svg
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="50 50 630.66 166">
|
||||
<defs>
|
||||
<style>
|
||||
path {
|
||||
fill: #2dbbed;
|
||||
stroke-width: 0px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #edeced; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path d="M137.38,167.9h-73.35c-1.64,0-2.97,1.33-2.97,2.97v24.6c0,1.64,1.33,2.97,2.97,2.97h9.33c1.64,0,2.97-1.33,2.97-2.97v-12.3h61.06v12.3c0,1.64,1.33,2.97,2.97,2.97h9.33c1.64,0,2.97-1.33,2.97-2.97v-24.6c0-1.64-1.33-2.97-2.97-2.97h-12.3Z"/>
|
||||
<path d="M111.04,65.25c-2.31-2.31-6.09-2.31-8.39,0l-37.4,37.4c-2.31,2.31-4.2,6.87-4.2,10.13v33.92c0,3.26,2.67,5.94,5.94,5.94h79.71c3.26,0,5.94-2.67,5.94-5.94v-33.92c0-3.26-1.89-7.82-4.2-10.13l-37.4-37.4Z"/>
|
||||
<g>
|
||||
<path d="M211.92,76.32c4.22,0,8.13.76,11.73,2.29,3.6,1.52,6.65,3.63,9.15,6.32,2.5,2.69,4.45,5.91,5.85,9.68,1.39,3.77,2.09,7.84,2.09,12.22.05,4.33-.63,8.39-2.03,12.2-1.41,3.81-3.38,7.06-5.91,9.76-2.53,2.7-5.61,4.82-9.23,6.35-3.62,1.54-7.53,2.28-11.73,2.23-5.55.08-10.54-1.2-14.96-3.83-4.42-2.63-7.83-6.28-10.23-10.95-2.4-4.67-3.56-9.9-3.48-15.68-.05-4.33.63-8.39,2.03-12.2,1.41-3.81,3.37-7.06,5.89-9.78,2.52-2.71,5.58-4.84,9.19-6.37s7.49-2.28,11.64-2.23ZM211.99,125.68c4.98,0,8.92-1.69,11.83-5.06,2.91-3.38,4.36-7.97,4.36-13.79s-1.45-10.48-4.34-13.85c-2.89-3.36-6.84-5.04-11.85-5.04s-8.96,1.68-11.85,5.04c-2.89,3.36-4.34,7.98-4.34,13.85s1.45,10.48,4.34,13.83c2.89,3.35,6.84,5.03,11.85,5.03Z"/>
|
||||
<path d="M293.19,96.89c0,5.92-1.79,10.68-5.36,14.29-3.57,3.61-8.43,5.42-14.59,5.42h-11.69v19.79h-11.93v-59.02h23.78c6.18,0,11.02,1.74,14.53,5.22,3.51,3.48,5.26,8.25,5.26,14.29ZM280.59,96.66c0-2.5-.81-4.57-2.44-6.2-1.63-1.63-3.98-2.44-7.06-2.44h-9.54v18.19h9.54c3.1,0,5.46-.88,7.08-2.64,1.62-1.76,2.42-4.06,2.42-6.9Z"/>
|
||||
<path d="M338.75,125.17v11.22h-37.15v-59.02h37.15v11.34h-25.23v12.59h22.41v10.56h-22.41v13.3h25.23Z"/>
|
||||
<path d="M400.7,77.38v59.02h-11.85l-25.85-39.97v39.97h-11.85v-59.02h11.85l25.85,40.05v-40.05h11.85Z"/>
|
||||
<path d="M434.49,77.38h11.93v23.86l23.39.08v-23.94h12.01v59.02h-12.01v-24.44l-23.39-.16v24.6h-11.93v-59.02Z"/>
|
||||
<path d="M519.36,76.32c4.22,0,8.13.76,11.73,2.29,3.6,1.52,6.65,3.63,9.15,6.32,2.5,2.69,4.45,5.91,5.85,9.68,1.39,3.77,2.09,7.84,2.09,12.22.05,4.33-.63,8.39-2.03,12.2-1.41,3.81-3.38,7.06-5.91,9.76s-5.61,4.82-9.23,6.35c-3.62,1.54-7.53,2.28-11.73,2.23-5.55.08-10.54-1.2-14.96-3.83-4.42-2.63-7.83-6.28-10.23-10.95-2.4-4.67-3.56-9.9-3.48-15.68-.05-4.33.63-8.39,2.03-12.2,1.41-3.81,3.37-7.06,5.89-9.78,2.52-2.71,5.58-4.84,9.19-6.37s7.49-2.28,11.64-2.23ZM519.43,125.68c4.98,0,8.92-1.69,11.83-5.06,2.91-3.38,4.36-7.97,4.36-13.79s-1.45-10.48-4.34-13.85c-2.89-3.36-6.84-5.04-11.85-5.04s-8.96,1.68-11.85,5.04c-2.89,3.36-4.34,7.98-4.34,13.85s1.45,10.48,4.34,13.83c2.89,3.35,6.84,5.03,11.85,5.03Z"/>
|
||||
<path d="M616.62,77.38v59.02h-11.77v-32.27l-12.63,32.27h-11.3l-12.48-32.03v32.03h-11.38v-59.02h11.38l18.15,45.09,18.26-45.09h11.77Z"/>
|
||||
<path d="M666.29,125.17v11.22h-37.15v-59.02h37.15v11.34h-25.23v12.59h22.41v10.56h-22.41v13.3h25.23Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M189.92,159.75v13.29h17.01v6.07h-17.01v18.53h-6.75v-44.28h26.97v6.39h-20.22Z"/>
|
||||
<path d="M235.89,152.64c3.07-.04,5.95.52,8.62,1.67s4.95,2.74,6.83,4.77c1.87,2.03,3.34,4.47,4.4,7.3,1.06,2.84,1.57,5.87,1.53,9.1.04,3.25-.47,6.31-1.53,9.16-1.06,2.86-2.53,5.29-4.4,7.32-1.87,2.02-4.15,3.61-6.83,4.76-2.68,1.15-5.55,1.71-8.62,1.67-3.07.04-5.94-.52-8.61-1.67-2.67-1.15-4.94-2.74-6.81-4.77-1.87-2.03-3.34-4.47-4.39-7.3-1.05-2.83-1.56-5.87-1.52-9.1-.04-3.23.47-6.27,1.52-9.11,1.05-2.84,2.51-5.28,4.39-7.32,1.87-2.03,4.14-3.63,6.81-4.79,2.67-1.16,5.54-1.72,8.61-1.68ZM225.59,187.27c2.61,2.96,6.06,4.45,10.36,4.45s7.75-1.48,10.35-4.45c2.6-2.96,3.9-6.89,3.9-11.79s-1.3-8.86-3.9-11.82c-2.6-2.96-6.05-4.45-10.35-4.45s-7.76,1.49-10.36,4.46c-2.61,2.97-3.91,6.91-3.91,11.81s1.3,8.83,3.91,11.79Z"/>
|
||||
<path d="M264.38,153.35h6.75v28.31c.02,3.37,1.01,5.92,2.97,7.64,1.96,1.73,4.54,2.59,7.73,2.59s5.63-.91,7.67-2.72c2.04-1.81,3.06-4.32,3.06-7.51v-28.31h6.75v28.58c0,2.6-.47,4.95-1.4,7.06-.93,2.11-2.2,3.85-3.79,5.2-1.6,1.36-3.44,2.4-5.55,3.14s-4.35,1.1-6.75,1.1-4.55-.36-6.63-1.07c-2.08-.71-3.94-1.74-5.56-3.08-1.63-1.34-2.91-3.07-3.85-5.2-.94-2.13-1.41-4.52-1.41-7.15v-28.58Z"/>
|
||||
<path d="M344.44,153.35v44.28h-6.75l-21.83-33.37v33.37h-6.75v-44.28h6.75l21.83,33.43v-33.43h6.75Z"/>
|
||||
<path d="M391.61,175.54c.02,3.17-.53,6.15-1.65,8.92-1.12,2.78-2.67,5.14-4.64,7.08-1.97,1.94-4.34,3.46-7.11,4.55-2.77,1.09-5.72,1.61-8.88,1.55h-14.75v-44.28h14.75c4.16-.06,7.96.86,11.38,2.77,3.42,1.9,6.1,4.56,8.04,7.97,1.94,3.41,2.89,7.23,2.86,11.45ZM384.44,175.54c0-4.68-1.39-8.48-4.18-11.4-2.79-2.92-6.45-4.39-10.99-4.39h-7.88v31.61h7.88c4.58,0,8.25-1.45,11.02-4.36,2.77-2.9,4.15-6.73,4.15-11.46Z"/>
|
||||
<path d="M421.25,186.99h-17.67l-3.84,10.65h-6.93l16-44.28h7.26l16.09,44.28h-7.14l-3.78-10.65ZM419.29,181.25l-6.81-19.3-6.87,19.3h13.68Z"/>
|
||||
<path d="M462.47,159.75h-12.85v37.89h-6.81v-37.89h-12.85v-6.39h32.5v6.39Z"/>
|
||||
<path d="M468.57,197.63v-44.28h6.81v44.28h-6.81Z"/>
|
||||
<path d="M504.19,152.64c3.07-.04,5.95.52,8.62,1.67,2.68,1.15,4.95,2.74,6.83,4.77,1.87,2.03,3.34,4.47,4.4,7.3,1.06,2.84,1.57,5.87,1.53,9.1.04,3.25-.47,6.31-1.53,9.16-1.06,2.86-2.53,5.29-4.4,7.32-1.87,2.02-4.15,3.61-6.83,4.76-2.68,1.15-5.55,1.71-8.62,1.67-3.07.04-5.94-.52-8.61-1.67-2.67-1.15-4.94-2.74-6.81-4.77-1.87-2.03-3.34-4.47-4.39-7.3-1.05-2.83-1.56-5.87-1.52-9.1-.04-3.23.47-6.27,1.52-9.11,1.05-2.84,2.51-5.28,4.39-7.32,1.87-2.03,4.14-3.63,6.81-4.79,2.67-1.16,5.54-1.72,8.61-1.68ZM493.89,187.27c2.61,2.96,6.06,4.45,10.36,4.45s7.75-1.48,10.35-4.45c2.6-2.96,3.9-6.89,3.9-11.79s-1.3-8.86-3.9-11.82c-2.6-2.96-6.05-4.45-10.35-4.45s-7.76,1.49-10.36,4.46c-2.61,2.97-3.91,6.91-3.91,11.81s1.3,8.83,3.91,11.79Z"/>
|
||||
<path d="M568.43,153.35v44.28h-6.75l-21.83-33.37v33.37h-6.75v-44.28h6.75l21.83,33.43v-33.43h6.75Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.7 KiB |
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240710.0"
|
||||
version = "20240731.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -25,7 +25,9 @@ export const navigate = (path: string, options?: NavigateOptions) => {
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
|
||||
mainWindow.history.state?.root
|
||||
? { root: true }
|
||||
: (options?.data ?? null),
|
||||
"",
|
||||
`${mainWindow.location.pathname}#${path}`
|
||||
);
|
||||
@ -34,7 +36,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
|
||||
}
|
||||
} else if (replace) {
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
|
||||
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { LitElement, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import "./ha-progress-button";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@ -17,7 +18,9 @@ class HaCallServiceButton extends LitElement {
|
||||
|
||||
@property() public service!: string;
|
||||
|
||||
@property({ type: Object }) public serviceData = {};
|
||||
@property({ type: Object }) public target!: HassServiceTarget;
|
||||
|
||||
@property({ type: Object }) public data = {};
|
||||
|
||||
@property() public confirmation?;
|
||||
|
||||
@ -39,7 +42,8 @@ class HaCallServiceButton extends LitElement {
|
||||
const eventData = {
|
||||
domain: this.domain,
|
||||
service: this.service,
|
||||
serviceData: this.serviceData,
|
||||
data: this.data,
|
||||
target: this.target,
|
||||
success: false,
|
||||
};
|
||||
|
||||
@ -47,7 +51,12 @@ class HaCallServiceButton extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-progress-button")!;
|
||||
|
||||
try {
|
||||
await this.hass.callService(this.domain, this.service, this.serviceData);
|
||||
await this.hass.callService(
|
||||
this.domain,
|
||||
this.service,
|
||||
this.data,
|
||||
this.target
|
||||
);
|
||||
this.progress = false;
|
||||
progressElement.actionSuccess();
|
||||
eventData.success = true;
|
||||
@ -85,7 +94,8 @@ declare global {
|
||||
"hass-service-called": {
|
||||
domain: string;
|
||||
service: string;
|
||||
serviceData: object;
|
||||
target: HassServiceTarget;
|
||||
data: object;
|
||||
success: boolean;
|
||||
};
|
||||
}
|
||||
|
@ -159,10 +159,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
},
|
||||
afterUpdate: (y) => {
|
||||
const yWidth = this.showNames
|
||||
? y.width ?? 0
|
||||
? (y.width ?? 0)
|
||||
: computeRTL(this.hass)
|
||||
? 0
|
||||
: y.left ?? 0;
|
||||
: (y.left ?? 0);
|
||||
if (
|
||||
this._yWidth !== Math.floor(yWidth) &&
|
||||
y.ticks.length === this.data.length
|
||||
|
@ -109,7 +109,8 @@ export class DialogDataTableSettings extends LitElement {
|
||||
const canHide = !col.main && col.hideable !== false;
|
||||
const isVisible = !(this._columnOrder &&
|
||||
this._columnOrder.includes(col.key)
|
||||
? this._hiddenColumns?.includes(col.key) ?? col.defaultHidden
|
||||
? (this._hiddenColumns?.includes(col.key) ??
|
||||
col.defaultHidden)
|
||||
: col.defaultHidden);
|
||||
|
||||
return html`<ha-list-item
|
||||
@ -193,6 +194,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
.filter(([_key, col]) => col.defaultHidden)
|
||||
.map(([key]) => key)),
|
||||
];
|
||||
|
||||
if (wasHidden && hidden.includes(column)) {
|
||||
hidden.splice(hidden.indexOf(column), 1);
|
||||
} else if (!wasHidden) {
|
||||
@ -242,7 +244,11 @@ export class DialogDataTableSettings extends LitElement {
|
||||
newOrder.splice(lastMoveable + 1, 0, col.key);
|
||||
}
|
||||
|
||||
if (col.defaultHidden) {
|
||||
if (
|
||||
col.key !== column &&
|
||||
col.defaultHidden &&
|
||||
!hidden.includes(col.key)
|
||||
) {
|
||||
hidden.push(col.key);
|
||||
}
|
||||
}
|
||||
|
@ -85,9 +85,9 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
| "flex";
|
||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
extraTemplate?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
flex?: number;
|
||||
forceLTR?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
@ -216,6 +216,18 @@ export class HaDataTable extends LitElement {
|
||||
this.updateComplete.then(() => this._calcTableHeight());
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
if (header.scrollWidth > header.clientWidth) {
|
||||
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
|
||||
} else {
|
||||
this.style.removeProperty("--table-row-width");
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
@ -355,7 +367,12 @@ export class HaDataTable extends LitElement {
|
||||
: `calc(100% - ${this._headerHeight}px)`,
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
||||
<div
|
||||
class="mdc-data-table__header-row"
|
||||
role="row"
|
||||
aria-rowindex="1"
|
||||
@scroll=${this._scrollContent}
|
||||
>
|
||||
<slot name="header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
@ -379,7 +396,8 @@ export class HaDataTable extends LitElement {
|
||||
if (
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
|
||||
? (this.hiddenColumns?.includes(key) ??
|
||||
column.defaultHidden)
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
return nothing;
|
||||
@ -397,18 +415,16 @@ export class HaDataTable extends LitElement {
|
||||
column.type === "overflow",
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
grows: Boolean(column.grows),
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
aria-label=${ifDefined(column.label)}
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth || "",
|
||||
})
|
||||
: ""}
|
||||
style=${styleMap({
|
||||
minWidth: column.minWidth,
|
||||
maxWidth: column.maxWidth,
|
||||
flex: column.flex || 1,
|
||||
})}
|
||||
role="columnheader"
|
||||
aria-sort=${ifDefined(
|
||||
sorted
|
||||
@ -518,7 +534,7 @@ export class HaDataTable extends LitElement {
|
||||
(narrow && !column.main && !column.showNarrow) ||
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
|
||||
? (this.hiddenColumns?.includes(key) ?? column.defaultHidden)
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
return nothing;
|
||||
@ -537,15 +553,13 @@ export class HaDataTable extends LitElement {
|
||||
"mdc-data-table__cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth ? column.maxWidth : "",
|
||||
})
|
||||
: ""}
|
||||
style=${styleMap({
|
||||
minWidth: column.minWidth,
|
||||
maxWidth: column.maxWidth,
|
||||
flex: column.flex || 1,
|
||||
})}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row)
|
||||
@ -560,8 +574,8 @@ export class HaDataTable extends LitElement {
|
||||
!column2.showNarrow &&
|
||||
!(this.columnOrder &&
|
||||
this.columnOrder.includes(key2)
|
||||
? this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden
|
||||
? (this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden)
|
||||
: column2.defaultHidden)
|
||||
)
|
||||
.map(
|
||||
@ -596,7 +610,7 @@ export class HaDataTable extends LitElement {
|
||||
filteredData = await this._memFilterData(
|
||||
this.data,
|
||||
this._sortColumns,
|
||||
this._filter
|
||||
this._filter.trim()
|
||||
);
|
||||
}
|
||||
|
||||
@ -814,6 +828,17 @@ export class HaDataTable extends LitElement {
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
|
||||
this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _scrollContent(e: Event) {
|
||||
this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
}
|
||||
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
@ -888,8 +913,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--data-table-row-height, 52px);
|
||||
width: var(--table-row-width, 100%);
|
||||
}
|
||||
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
@ -913,18 +938,26 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__header-row {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.mdc-data-table__header-row {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.mdc-data-table__cell,
|
||||
.mdc-data-table__header-cell {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
min-width: 150px;
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -972,6 +1005,8 @@ export class HaDataTable extends LitElement {
|
||||
letter-spacing: 0.0178571429em;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell a {
|
||||
@ -990,7 +1025,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__header-cell--icon,
|
||||
.mdc-data-table__cell--icon {
|
||||
width: 54px;
|
||||
min-width: 64px;
|
||||
flex: 0 0 64px !important;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon img {
|
||||
@ -1030,11 +1066,14 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__header-cell--overflow-menu,
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
min-width: 64px;
|
||||
flex: 0 0 64px !important;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-cell--icon-button,
|
||||
.mdc-data-table__cell--icon-button {
|
||||
min-width: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
|
315
src/components/entity/ha-entity-state-content-picker.ts
Normal file
315
src/components/entity/ha-entity-state-content-picker.ts
Normal file
@ -0,0 +1,315 @@
|
||||
import { mdiDrag } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import {
|
||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
|
||||
} from "../../state-display/state-display";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
"access_token",
|
||||
"available_modes",
|
||||
"battery_icon",
|
||||
"battery_level",
|
||||
"code_arm_required",
|
||||
"code_format",
|
||||
"color_modes",
|
||||
"device_class",
|
||||
"editable",
|
||||
"effect_list",
|
||||
"entity_id",
|
||||
"entity_picture",
|
||||
"event_types",
|
||||
"fan_modes",
|
||||
"fan_speed_list",
|
||||
"friendly_name",
|
||||
"frontend_stream_type",
|
||||
"has_date",
|
||||
"has_time",
|
||||
"hvac_modes",
|
||||
"icon",
|
||||
"id",
|
||||
"max_color_temp_kelvin",
|
||||
"max_mireds",
|
||||
"max_temp",
|
||||
"max",
|
||||
"min_color_temp_kelvin",
|
||||
"min_mireds",
|
||||
"min_temp",
|
||||
"min",
|
||||
"mode",
|
||||
"operation_list",
|
||||
"options",
|
||||
"percentage_step",
|
||||
"precipitation_unit",
|
||||
"preset_modes",
|
||||
"pressure_unit",
|
||||
"remaining",
|
||||
"sound_mode_list",
|
||||
"source_list",
|
||||
"state_class",
|
||||
"step",
|
||||
"supported_color_modes",
|
||||
"supported_features",
|
||||
"swing_modes",
|
||||
"target_temp_step",
|
||||
"temperature_unit",
|
||||
"token",
|
||||
"unit_of_measurement",
|
||||
"visibility_unit",
|
||||
"wind_speed_unit",
|
||||
];
|
||||
|
||||
@customElement("ha-entity-state-content-picker")
|
||||
class HaEntityStatePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
return !(!changedProps.has("_opened") && this._opened);
|
||||
}
|
||||
|
||||
private options = memoizeOne((entityId?: string, stateObj?: HassEntity) => {
|
||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||
return [
|
||||
{
|
||||
label: this.hass.localize("ui.components.state-content-picker.state"),
|
||||
value: "state",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_changed"
|
||||
),
|
||||
value: "last_changed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.state-content-picker.last_updated"
|
||||
),
|
||||
value: "last_updated",
|
||||
},
|
||||
...(domain
|
||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||
).map((content) => ({
|
||||
label: this.hass.localize(
|
||||
`ui.components.state-content-picker.${content}`
|
||||
),
|
||||
value: content,
|
||||
}))
|
||||
: []),
|
||||
...Object.keys(stateObj?.attributes ?? {})
|
||||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
||||
.map((attribute) => ({
|
||||
value: attribute,
|
||||
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
private _filter = "";
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const value = this._value;
|
||||
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
|
||||
const options = this.options(this.entityId, stateObj);
|
||||
const optionItems = options.filter(
|
||||
(option) => !this._value.includes(option.value)
|
||||
);
|
||||
|
||||
return html`
|
||||
${value?.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)?.label ||
|
||||
item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
selected
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
|
||||
${label}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-combo-box
|
||||
item-value-path="value"
|
||||
item-label-path="label"
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${""}
|
||||
.items=${optionItems}
|
||||
allow-custom-value
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return !this.value ? [] : ensureArray(this.value);
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
this._comboBox.filteredItems = this._comboBox.items;
|
||||
}
|
||||
|
||||
private _filterChanged(ev?: CustomEvent): void {
|
||||
this._filter = ev?.detail.value || "";
|
||||
|
||||
const filteredItems = this._comboBox.items?.filter((item) => {
|
||||
const label = item.label || item.value;
|
||||
return label.toLowerCase().includes(this._filter?.toLowerCase());
|
||||
});
|
||||
|
||||
if (this._filter) {
|
||||
filteredItems?.unshift({ label: this._filter, value: this._filter });
|
||||
}
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._value;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
await this.updateComplete;
|
||||
this._filterChanged();
|
||||
}
|
||||
|
||||
private async _removeItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const value: string[] = [...this._value];
|
||||
value.splice(ev.target.idx, 1);
|
||||
this._setValue(value);
|
||||
await this.updateComplete;
|
||||
this._filterChanged();
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (this.disabled || newValue === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = this._value;
|
||||
|
||||
if (currentValue.includes(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._filterChanged();
|
||||
this._comboBox.setInputValue("");
|
||||
}, 0);
|
||||
|
||||
this._setValue([...currentValue, newValue]);
|
||||
}
|
||||
|
||||
private _setValue(value: string[]) {
|
||||
const newValue =
|
||||
value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-state-content-picker": HaEntityStatePicker;
|
||||
}
|
||||
}
|
@ -134,7 +134,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
this._timerTimeRemaining
|
||||
)}
|
||||
.description=${this.showName
|
||||
? this.name ?? computeStateName(entityState)
|
||||
? (this.name ?? computeStateName(entityState))
|
||||
: undefined}
|
||||
>
|
||||
${!image && showIcon
|
||||
|
@ -279,6 +279,8 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -295,6 +297,8 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: "mdi:plus",
|
||||
aliases: [],
|
||||
labels: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -377,6 +381,8 @@ export class HaAreaPicker extends LitElement {
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
] as AreaRegistryEntry[];
|
||||
} else {
|
||||
@ -393,6 +399,8 @@ export class HaAreaPicker extends LitElement {
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
] as AreaRegistryEntry[];
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, TemplateResult, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-select";
|
||||
import "./ha-icon-button";
|
||||
import { HaTextField } from "./ha-textfield";
|
||||
import "./ha-input-helper-text";
|
||||
|
||||
@ -124,116 +126,128 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property() amPm: "AM" | "PM" = "AM";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label
|
||||
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
|
||||
: ""}
|
||||
<div class="time-input-wrap">
|
||||
${this.enableDay
|
||||
? html`
|
||||
<ha-textfield
|
||||
id="day"
|
||||
<div class="time-input-wrap-wrap">
|
||||
<div class="time-input-wrap">
|
||||
${this.enableDay
|
||||
? html`
|
||||
<ha-textfield
|
||||
id="day"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.days.toFixed()}
|
||||
.label=${this.dayLabel}
|
||||
name="days"
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
id="hour"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.hours.toFixed()}
|
||||
.label=${this.hourLabel}
|
||||
name="hours"
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max=${ifDefined(this._hourMax)}
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
id="min"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this._formatValue(this.minutes)}
|
||||
.label=${this.minLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="minutes"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
.suffix=${this.enableSecond ? ":" : ""}
|
||||
class=${this.enableSecond ? "has-suffix" : ""}
|
||||
>
|
||||
</ha-textfield>
|
||||
${this.enableSecond
|
||||
? html`<ha-textfield
|
||||
id="sec"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.days.toFixed()}
|
||||
.label=${this.dayLabel}
|
||||
name="days"
|
||||
.value=${this._formatValue(this.seconds)}
|
||||
.label=${this.secLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="seconds"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
.suffix=${this.enableMillisecond ? ":" : ""}
|
||||
class=${this.enableMillisecond ? "has-suffix" : ""}
|
||||
>
|
||||
</ha-textfield>
|
||||
`
|
||||
: ""}
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
${this.enableMillisecond
|
||||
? html`<ha-textfield
|
||||
id="millisec"
|
||||
type="number"
|
||||
.value=${this._formatValue(this.milliseconds, 3)}
|
||||
.label=${this.millisecLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="milliseconds"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="3"
|
||||
max="999"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
${this.clearable && !this.required && !this.disabled
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@click=${this._clearValue}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<ha-textfield
|
||||
id="hour"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this.hours.toFixed()}
|
||||
.label=${this.hourLabel}
|
||||
name="hours"
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max=${ifDefined(this._hourMax)}
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
suffix=":"
|
||||
class="hasSuffix"
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
id="min"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this._formatValue(this.minutes)}
|
||||
.label=${this.minLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="minutes"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
.suffix=${this.enableSecond ? ":" : ""}
|
||||
class=${this.enableSecond ? "has-suffix" : ""}
|
||||
>
|
||||
</ha-textfield>
|
||||
${this.enableSecond
|
||||
? html`<ha-textfield
|
||||
id="sec"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
.value=${this._formatValue(this.seconds)}
|
||||
.label=${this.secLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="seconds"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="2"
|
||||
max="59"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
.suffix=${this.enableMillisecond ? ":" : ""}
|
||||
class=${this.enableMillisecond ? "has-suffix" : ""}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
${this.enableMillisecond
|
||||
? html`<ha-textfield
|
||||
id="millisec"
|
||||
type="number"
|
||||
.value=${this._formatValue(this.milliseconds, 3)}
|
||||
.label=${this.millisecLabel}
|
||||
@change=${this._valueChanged}
|
||||
@focusin=${this._onFocus}
|
||||
name="milliseconds"
|
||||
no-spinner
|
||||
.required=${this.required}
|
||||
.autoValidate=${this.autoValidate}
|
||||
maxlength="3"
|
||||
max="999"
|
||||
min="0"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
</ha-textfield>`
|
||||
: ""}
|
||||
${this.format === 24
|
||||
? ""
|
||||
: html`<ha-select
|
||||
@ -249,13 +263,17 @@ export class HaBaseTimeInput extends LitElement {
|
||||
<mwc-list-item value="AM">AM</mwc-list-item>
|
||||
<mwc-list-item value="PM">PM</mwc-list-item>
|
||||
</ha-select>`}
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""}
|
||||
</div>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
@ -302,18 +320,25 @@ export class HaBaseTimeInput extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.time-input-wrap-wrap {
|
||||
display: flex;
|
||||
}
|
||||
.time-input-wrap {
|
||||
display: flex;
|
||||
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
direction: ltr;
|
||||
padding-right: 3px;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 40px;
|
||||
width: 55px;
|
||||
text-align: center;
|
||||
--mdc-shape-small: 0;
|
||||
--text-field-appearance: none;
|
||||
@ -335,6 +360,21 @@ export class HaBaseTimeInput extends LitElement {
|
||||
--mdc-shape-small: 0;
|
||||
width: 85px;
|
||||
}
|
||||
:host([clearable]) .mdc-select__anchor {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: relative
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color:var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
label {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@ -295,6 +295,8 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -309,6 +311,8 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
icon: "mdi:plus",
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -391,6 +395,8 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
] as FloorRegistryEntry[];
|
||||
} else {
|
||||
@ -405,6 +411,8 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
icon: "mdi:plus",
|
||||
level: null,
|
||||
aliases: [],
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
] as FloorRegistryEntry[];
|
||||
}
|
||||
|
@ -94,6 +94,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = selector.color_temp?.min_mireds ?? 153;
|
||||
} else if (
|
||||
"action" in selector ||
|
||||
"trigger" in selector ||
|
||||
"condition" in selector ||
|
||||
"media" in selector ||
|
||||
"target" in selector
|
||||
) {
|
||||
|
@ -20,7 +20,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: GridSizeValue;
|
||||
|
||||
@property({ attribute: false }) public rows = 6;
|
||||
@property({ attribute: false }) public rows = 8;
|
||||
|
||||
@property({ attribute: false }) public columns = 4;
|
||||
|
||||
@ -205,7 +205,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.preview {
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1 / 1;
|
||||
aspect-ratio: 1 / 1.2;
|
||||
}
|
||||
.preview > div {
|
||||
position: absolute;
|
||||
|
@ -303,6 +303,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
icon: null,
|
||||
color: null,
|
||||
description: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -317,6 +319,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
icon: "mdi:plus",
|
||||
color: null,
|
||||
description: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ import {
|
||||
mdiSofa,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@ -20,7 +20,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||
import { ConfigEntry, getConfigEntries } from "../data/config_entries";
|
||||
import { findRelated, ItemType, RelatedResult } from "../data/search";
|
||||
import { ItemType, RelatedResult, findRelated } from "../data/search";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
@ -109,6 +109,26 @@ export class HaRelatedItems extends LitElement {
|
||||
)
|
||||
);
|
||||
|
||||
private _getConfigEntries = memoizeOne(
|
||||
(
|
||||
relatedConfigEntries: string[] | undefined,
|
||||
entries: ConfigEntry[] | undefined
|
||||
) => {
|
||||
const configEntries =
|
||||
relatedConfigEntries && entries
|
||||
? relatedConfigEntries.map((entryId) =>
|
||||
entries!.find((configEntry) => configEntry.entry_id === entryId)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const configEntryDomains = new Set(
|
||||
configEntries?.map((entry) => entry?.domain)
|
||||
);
|
||||
|
||||
return { configEntries, configEntryDomains };
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._related) {
|
||||
return nothing;
|
||||
@ -128,22 +148,25 @@ export class HaRelatedItems extends LitElement {
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
const { configEntries, configEntryDomains } = this._getConfigEntries(
|
||||
this._related.config_entry,
|
||||
this._entries
|
||||
);
|
||||
|
||||
return html`
|
||||
${this._related.config_entry && this._entries
|
||||
${configEntries || this._related.integration
|
||||
? html`<h3>
|
||||
${this.hass.localize("ui.components.related-items.integration")}
|
||||
</h3>
|
||||
<mwc-list
|
||||
>${this._related.config_entry.map((relatedConfigEntryId) => {
|
||||
const entry: ConfigEntry | undefined = this._entries!.find(
|
||||
(configEntry) => configEntry.entry_id === relatedConfigEntryId
|
||||
);
|
||||
>${configEntries?.map((entry) => {
|
||||
if (!entry) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<a
|
||||
href=${`/config/integrations/integration/${entry.domain}#config_entry=${relatedConfigEntryId}`}
|
||||
href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
@ -164,8 +187,34 @@ export class HaRelatedItems extends LitElement {
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`;
|
||||
})}</mwc-list
|
||||
>`
|
||||
})}
|
||||
${this._related.integration
|
||||
?.filter((integration) => !configEntryDomains.has(integration))
|
||||
.map(
|
||||
(integration) =>
|
||||
html`<a
|
||||
href=${`/config/integrations/integration/${integration}`}
|
||||
@click=${this._navigateAwayClose}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
.src=${brandsUrl({
|
||||
domain: integration,
|
||||
type: "icon",
|
||||
useFallback: true,
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${integration}
|
||||
slot="graphic"
|
||||
/>
|
||||
${this.hass.localize(`component.${integration}.title`)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
)}
|
||||
</mwc-list>`
|
||||
: nothing}
|
||||
${this._related.device
|
||||
? html`<h3>
|
||||
|
@ -12,6 +12,8 @@ export class HaBooleanSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public value = false;
|
||||
|
||||
@property() public placeholder?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
@ -22,7 +24,7 @@ export class HaBooleanSelector extends LitElement {
|
||||
return html`
|
||||
<ha-formfield alignEnd spaceBetween .label=${this.label}>
|
||||
<ha-switch
|
||||
.checked=${this.value}
|
||||
.checked=${this.value ?? this.placeholder === true}
|
||||
@change=${this._handleChange}
|
||||
.disabled=${this.disabled}
|
||||
></ha-switch>
|
||||
|
@ -30,6 +30,7 @@ export class HaTimeDuration extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
?enableDay=${this.selector.duration?.enable_day}
|
||||
?enableMillisecond=${this.selector.duration?.enable_millisecond}
|
||||
></ha-duration-input>
|
||||
`;
|
||||
}
|
||||
|
@ -57,6 +57,10 @@ const SELECTOR_SCHEMAS = {
|
||||
name: "enable_day",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "enable_millisecond",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
entity: [
|
||||
{
|
||||
|
@ -27,6 +27,7 @@ export class HaTimeSelector extends LitElement {
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
enable-second
|
||||
|
48
src/components/ha-selector/ha-selector-ui-state-content.ts
Normal file
48
src/components/ha-selector/ha-selector-ui-state-content.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { UiStateContentSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-content-picker";
|
||||
|
||||
@customElement("ha-selector-ui_state_content")
|
||||
export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: UiStateContentSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-state-content-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.ui_state_content?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-entity-state-content-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_state_content": HaSelectorUiStateContent;
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ const LOAD_ELEMENTS = {
|
||||
color_temp: () => import("./ha-selector-color-temp"),
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
};
|
||||
|
||||
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
||||
|
@ -77,7 +77,7 @@ export class HaServiceControl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
service: string;
|
||||
action: string;
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
@ -112,23 +112,23 @@ export class HaServiceControl extends LitElement {
|
||||
| undefined
|
||||
| this["value"];
|
||||
|
||||
if (oldValue?.service !== this.value?.service) {
|
||||
if (oldValue?.action !== this.value?.action) {
|
||||
this._checkedKeys = new Set();
|
||||
}
|
||||
|
||||
const serviceData = this._getServiceInfo(
|
||||
this.value?.service,
|
||||
this.value?.action,
|
||||
this.hass.services
|
||||
);
|
||||
|
||||
// Fetch the manifest if we have a service selected and the service domain changed.
|
||||
// If no service is selected, clear the manifest.
|
||||
if (this.value?.service) {
|
||||
if (this.value?.action) {
|
||||
if (
|
||||
!oldValue?.service ||
|
||||
computeDomain(this.value.service) !== computeDomain(oldValue.service)
|
||||
!oldValue?.action ||
|
||||
computeDomain(this.value.action) !== computeDomain(oldValue.action)
|
||||
) {
|
||||
this._fetchManifest(computeDomain(this.value?.service));
|
||||
this._fetchManifest(computeDomain(this.value?.action));
|
||||
}
|
||||
} else {
|
||||
this._manifest = undefined;
|
||||
@ -168,7 +168,7 @@ export class HaServiceControl extends LitElement {
|
||||
this._value = this.value;
|
||||
}
|
||||
|
||||
if (oldValue?.service !== this.value?.service) {
|
||||
if (oldValue?.action !== this.value?.action) {
|
||||
let updatedDefaultValue = false;
|
||||
if (this._value && serviceData) {
|
||||
const loadDefaults = this.value && !("data" in this.value);
|
||||
@ -367,7 +367,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const serviceData = this._getServiceInfo(
|
||||
this._value?.service,
|
||||
this._value?.action,
|
||||
this.hass.services
|
||||
);
|
||||
|
||||
@ -392,11 +392,11 @@ export class HaServiceControl extends LitElement {
|
||||
this._value
|
||||
);
|
||||
|
||||
const domain = this._value?.service
|
||||
? computeDomain(this._value.service)
|
||||
const domain = this._value?.action
|
||||
? computeDomain(this._value.action)
|
||||
: undefined;
|
||||
const serviceName = this._value?.service
|
||||
? computeObjectId(this._value.service)
|
||||
const serviceName = this._value?.action
|
||||
? computeObjectId(this._value.action)
|
||||
: undefined;
|
||||
|
||||
const description =
|
||||
@ -410,7 +410,7 @@ export class HaServiceControl extends LitElement {
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.value=${this._value?.action}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>`}
|
||||
@ -451,7 +451,7 @@ export class HaServiceControl extends LitElement {
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_description"
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
@ -478,7 +478,9 @@ export class HaServiceControl extends LitElement {
|
||||
${shouldRenderServiceDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-control.data")}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this._value?.data}
|
||||
@ -594,11 +596,11 @@ export class HaServiceControl extends LitElement {
|
||||
};
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this._value?.service) {
|
||||
if (!this._value?.action) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this._value.service)}.selector.${key}`
|
||||
`component.${computeDomain(this._value.action)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
@ -610,7 +612,7 @@ export class HaServiceControl extends LitElement {
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
const field = this._getServiceInfo(
|
||||
this._value?.service,
|
||||
this._value?.action,
|
||||
this.hass.services
|
||||
)?.fields.find((_field) => _field.key === key);
|
||||
|
||||
@ -656,7 +658,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _serviceChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value === this._value?.service) {
|
||||
if (ev.detail.value === this._value?.action) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -715,7 +717,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
|
||||
const value = {
|
||||
service: newService,
|
||||
action: newService,
|
||||
target,
|
||||
};
|
||||
|
||||
|
@ -46,7 +46,7 @@ class HaServicePicker extends LitElement {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-picker.service")}
|
||||
.label=${this.hass.localize("ui.components.service-picker.action")}
|
||||
.filteredItems=${this._filteredServices(
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
|
@ -210,6 +210,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _editStyleLoaded = false;
|
||||
|
||||
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
|
||||
|
||||
@storage({
|
||||
key: "sidebarPanelOrder",
|
||||
state: true,
|
||||
@ -283,15 +285,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
hass.localize !== oldHass.localize ||
|
||||
hass.locale !== oldHass.locale ||
|
||||
hass.states !== oldHass.states ||
|
||||
hass.defaultPanel !== oldHass.defaultPanel
|
||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
||||
hass.connected !== oldHass.connected
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
this.subscribePersistentNotifications();
|
||||
}
|
||||
|
||||
private subscribePersistentNotifications(): void {
|
||||
if (this._unsubPersistentNotifications) {
|
||||
this._unsubPersistentNotifications();
|
||||
}
|
||||
this._unsubPersistentNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
this._notifications = notifications;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
@ -306,6 +319,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.hass &&
|
||||
changedProps.get("hass")?.connected === false &&
|
||||
this.hass.connected === true
|
||||
) {
|
||||
this.subscribePersistentNotifications();
|
||||
}
|
||||
|
||||
this._calculateCounts();
|
||||
|
||||
if (!SUPPORT_SCROLL_IF_NEEDED) {
|
||||
|
@ -15,6 +15,7 @@ export class HaSlider extends MdSlider {
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
--md-sys-color-on-primary: var(--text-primary-color);
|
||||
--md-sys-color-outline: var(--outline-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-slider-handle-width: 14px;
|
||||
|
@ -23,6 +23,8 @@ export class HaTimeInput extends LitElement {
|
||||
@property({ type: Boolean, attribute: "enable-second" })
|
||||
public enableSecond = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
protected render() {
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
|
||||
@ -48,22 +50,26 @@ export class HaTimeInput extends LitElement {
|
||||
@value-changed=${this._timeChanged}
|
||||
.enableSecond=${this.enableSecond}
|
||||
.required=${this.required}
|
||||
.clearable=${this.clearable && this.value !== undefined}
|
||||
.helper=${this.helper}
|
||||
></ha-base-time-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: TimeChangedEvent }>) {
|
||||
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
ev.stopPropagation();
|
||||
const eventValue = ev.detail.value;
|
||||
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
let value;
|
||||
let value: string | undefined;
|
||||
|
||||
// An undefined eventValue means the time selector is being cleared,
|
||||
// the `value` variable will (intentionally) be left undefined.
|
||||
if (
|
||||
!isNaN(eventValue.hours) ||
|
||||
!isNaN(eventValue.minutes) ||
|
||||
!isNaN(eventValue.seconds)
|
||||
eventValue !== undefined &&
|
||||
(!isNaN(eventValue.hours) ||
|
||||
!isNaN(eventValue.minutes) ||
|
||||
!isNaN(eventValue.seconds))
|
||||
) {
|
||||
let hours = eventValue.hours || 0;
|
||||
if (eventValue && useAMPM) {
|
||||
|
@ -49,6 +49,8 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public copyClipboard = false;
|
||||
|
||||
@property({ type: Boolean }) public hasExtraActions = false;
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
public setValue(value): void {
|
||||
@ -100,13 +102,16 @@ export class HaYamlEditor extends LitElement {
|
||||
@value-changed=${this._onChange}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
${this.copyClipboard
|
||||
${this.copyClipboard || this.hasExtraActions
|
||||
? html`<div class="card-actions">
|
||||
<mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>
|
||||
${this.copyClipboard
|
||||
? html` <mwc-button @click=${this._copyYaml}>
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: nothing}
|
||||
<slot name="extra-actions"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
|
@ -483,12 +483,12 @@ export class HaMap extends ReactiveElement {
|
||||
const entityName =
|
||||
typeof entity !== "string" && entity.label_mode === "state"
|
||||
? this.hass.formatEntityState(stateObj)
|
||||
: customTitle ??
|
||||
: (customTitle ??
|
||||
title
|
||||
.split(" ")
|
||||
.map((part) => part[0])
|
||||
.join("")
|
||||
.substr(0, 3);
|
||||
.substr(0, 3));
|
||||
|
||||
// create marker with the icon
|
||||
const marker = Leaflet.marker([latitude, longitude], {
|
||||
|
@ -79,7 +79,7 @@ class SearchInputOutlined extends LitElement {
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value?.trim());
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
|
@ -67,7 +67,7 @@ class SearchInput extends LitElement {
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value?.trim());
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
|
@ -17,7 +17,7 @@ export class HaTileIcon extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
--tile-icon-color: var(--disabled-color);
|
||||
--mdc-icon-size: 24px;
|
||||
--mdc-icon-size: 22px;
|
||||
}
|
||||
.shape::before {
|
||||
content: "";
|
||||
@ -32,9 +32,9 @@ export class HaTileIcon extends LitElement {
|
||||
}
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -25,9 +25,9 @@ export class HaTileImage extends LitElement {
|
||||
return css`
|
||||
.image {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
|
@ -33,7 +33,7 @@ export class HaTileInfo extends LitElement {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
height: 36px;
|
||||
}
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
|
@ -424,7 +424,7 @@ export class HatScriptGraph extends LitElement {
|
||||
return html`
|
||||
<hat-graph-node
|
||||
.graphStart=${graphStart}
|
||||
.iconPath=${node.service ? undefined : mdiRoomService}
|
||||
.iconPath=${node.action ? undefined : mdiRoomService}
|
||||
@focus=${this.selectNode(node, path)}
|
||||
?track=${path in this.trace.trace}
|
||||
?active=${this.selected === path}
|
||||
@ -432,11 +432,11 @@ export class HatScriptGraph extends LitElement {
|
||||
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
|
||||
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
|
||||
>
|
||||
${node.service
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.service}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
</hat-graph-node>
|
||||
|
@ -2,10 +2,11 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry {
|
||||
export interface AreaRegistryEntry extends RegistryEntry {
|
||||
area_id: string;
|
||||
floor_id: string | null;
|
||||
name: string;
|
||||
|
@ -6,7 +6,7 @@ import { navigate } from "../common/navigate";
|
||||
import { Context, HomeAssistant } from "../types";
|
||||
import { BlueprintInput } from "./blueprint";
|
||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import { Action, MODES } from "./script";
|
||||
import { Action, MODES, migrateAutomationAction } from "./script";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@ -28,7 +28,7 @@ export interface ManualAutomationConfig {
|
||||
description?: string;
|
||||
trigger: Trigger | Trigger[];
|
||||
condition?: Condition | Condition[];
|
||||
action: Action | Action[];
|
||||
action?: Action | Action[];
|
||||
mode?: (typeof MODES)[number];
|
||||
max?: number;
|
||||
max_exceeded?:
|
||||
@ -357,7 +357,7 @@ export const normalizeAutomationConfig = <
|
||||
>(
|
||||
config: T
|
||||
): T => {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Normalize data: ensure triggers, actions and conditions are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
@ -365,6 +365,9 @@ export const normalizeAutomationConfig = <
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
config.action = migrateAutomationAction(config.action || []);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
|
@ -115,8 +115,8 @@ export function computeCoverPositionStateDisplay(
|
||||
position?: number
|
||||
) {
|
||||
const statePosition = stateActive(stateObj)
|
||||
? stateObj.attributes.current_position ??
|
||||
stateObj.attributes.current_tilt_position
|
||||
? (stateObj.attributes.current_position ??
|
||||
stateObj.attributes.current_tilt_position)
|
||||
: undefined;
|
||||
|
||||
const currentPosition = position ?? statePosition;
|
||||
|
@ -178,7 +178,11 @@ const getEntityName = (
|
||||
entityId: string | undefined
|
||||
): string => {
|
||||
if (!entityId) {
|
||||
return "<unknown entity>";
|
||||
return (
|
||||
"<" +
|
||||
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
|
||||
">"
|
||||
);
|
||||
}
|
||||
if (entityId.includes(".")) {
|
||||
const state = hass.states[entityId];
|
||||
@ -191,7 +195,11 @@ const getEntityName = (
|
||||
if (entityReg) {
|
||||
return computeEntityRegistryName(hass, entityReg) || entityId;
|
||||
}
|
||||
return "<unknown entity>";
|
||||
return (
|
||||
"<" +
|
||||
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
|
||||
">"
|
||||
);
|
||||
};
|
||||
|
||||
export const localizeDeviceAutomationAction = (
|
||||
|
@ -1,25 +1,27 @@
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { ConfigEntry } from "./config_entries";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { ConfigEntry } from "./config_entries";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
import { RegistryEntry } from "./registry";
|
||||
|
||||
export {
|
||||
fetchDeviceRegistry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "./ws-device_registry";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
export interface DeviceRegistryEntry extends RegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
connections: Array<[string, string]>;
|
||||
identifiers: Array<[string, string]>;
|
||||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
model_id: string | null;
|
||||
name: string | null;
|
||||
labels: string[];
|
||||
sw_version: string | null;
|
||||
|
@ -7,6 +7,7 @@ import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LightColor } from "./light";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeEntityRegistryDisplay } from "./ws-entity_registry_display";
|
||||
|
||||
@ -43,7 +44,7 @@ export interface EntityRegistryDisplayEntryResponse {
|
||||
entity_categories: Record<number, EntityCategory>;
|
||||
}
|
||||
|
||||
export interface EntityRegistryEntry {
|
||||
export interface EntityRegistryEntry extends RegistryEntry {
|
||||
id: string;
|
||||
entity_id: string;
|
||||
name: string | null;
|
||||
|
@ -4,10 +4,11 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
import { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface FloorRegistryEntry {
|
||||
export interface FloorRegistryEntry extends RegistryEntry {
|
||||
floor_id: string;
|
||||
name: string;
|
||||
level: number | null;
|
||||
|
@ -1,10 +1,8 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
interface GroupEntityAttributes extends HassEntityAttributeBase {
|
||||
entity_id: string[];
|
||||
@ -17,11 +15,6 @@ export interface GroupEntity extends HassEntityBase {
|
||||
attributes: GroupEntityAttributes;
|
||||
}
|
||||
|
||||
export interface GroupPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const computeGroupDomain = (
|
||||
stateObj: GroupEntity
|
||||
): string | undefined => {
|
||||
@ -31,17 +24,3 @@ export const computeGroupDomain = (
|
||||
];
|
||||
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
|
||||
};
|
||||
|
||||
export const subscribePreviewGroup = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: GroupPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "group/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { RegistryEntry } from "./registry";
|
||||
|
||||
export interface LabelRegistryEntry {
|
||||
export interface LabelRegistryEntry extends RegistryEntry {
|
||||
label_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
|
@ -3,9 +3,10 @@ import {
|
||||
getCollection,
|
||||
HassEventBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HuiBadge } from "../panels/lovelace/badges/hui-badge";
|
||||
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
|
||||
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||
import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
|
||||
import { Lovelace } from "../panels/lovelace/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
|
||||
@ -21,7 +22,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
narrow?: boolean;
|
||||
index?: number;
|
||||
cards?: HuiCard[];
|
||||
badges?: LovelaceBadge[];
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
|
@ -5,10 +5,12 @@ export interface ToggleActionConfig extends BaseActionConfig {
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: "call-service";
|
||||
service: string;
|
||||
action: "call-service" | "perform-action";
|
||||
/** @deprecated "service" is kept for backwards compatibility. Replaced by "perform_action". */
|
||||
service?: string;
|
||||
perform_action: string;
|
||||
target?: HassServiceTarget;
|
||||
// "service_data" is kept for backwards compatibility. Replaced by "data".
|
||||
/** @deprecated "service_data" is kept for backwards compatibility. Replaced by "data". */
|
||||
service_data?: Record<string, unknown>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
@ -1,4 +1,25 @@
|
||||
import { Condition } from "../../../panels/lovelace/common/validate-condition";
|
||||
|
||||
export interface LovelaceBadgeConfig {
|
||||
type?: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
visibility?: Condition[];
|
||||
}
|
||||
|
||||
export const ensureBadgeConfig = (
|
||||
config: Partial<LovelaceBadgeConfig> | string
|
||||
): LovelaceBadgeConfig => {
|
||||
if (typeof config === "string") {
|
||||
return {
|
||||
type: "entity",
|
||||
entity: config,
|
||||
};
|
||||
}
|
||||
if ("type" in config && config.type) {
|
||||
return config as LovelaceBadgeConfig;
|
||||
}
|
||||
return {
|
||||
type: "entity",
|
||||
...config,
|
||||
};
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig {
|
||||
|
||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
type?: string;
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
badges?: (string | Partial<LovelaceBadgeConfig>)[]; // Badge can be just an entity_id or without type
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
}
|
||||
|
@ -8,6 +8,14 @@ export interface CustomCardEntry {
|
||||
documentationURL?: string;
|
||||
}
|
||||
|
||||
export interface CustomBadgeEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
}
|
||||
|
||||
export interface CustomCardFeatureEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
@ -18,6 +26,7 @@ export interface CustomCardFeatureEntry {
|
||||
export interface CustomCardsWindow {
|
||||
customCards?: CustomCardEntry[];
|
||||
customCardFeatures?: CustomCardFeatureEntry[];
|
||||
customBadges?: CustomBadgeEntry[];
|
||||
/**
|
||||
* @deprecated Use customCardFeatures
|
||||
*/
|
||||
@ -34,6 +43,9 @@ if (!("customCards" in customCardsWindow)) {
|
||||
if (!("customCardFeatures" in customCardsWindow)) {
|
||||
customCardsWindow.customCardFeatures = [];
|
||||
}
|
||||
if (!("customBadges" in customCardsWindow)) {
|
||||
customCardsWindow.customBadges = [];
|
||||
}
|
||||
if (!("customTileFeatures" in customCardsWindow)) {
|
||||
customCardsWindow.customTileFeatures = [];
|
||||
}
|
||||
@ -43,10 +55,14 @@ export const getCustomCardFeatures = () => [
|
||||
...customCardsWindow.customCardFeatures!,
|
||||
...customCardsWindow.customTileFeatures!,
|
||||
];
|
||||
export const customBadges = customCardsWindow.customBadges!;
|
||||
|
||||
export const getCustomCardEntry = (type: string) =>
|
||||
customCards.find((card) => card.type === type);
|
||||
|
||||
export const getCustomBadgeEntry = (type: string) =>
|
||||
customBadges.find((badge) => badge.type === type);
|
||||
|
||||
export const isCustomType = (type: string) =>
|
||||
type.startsWith(CUSTOM_TYPE_PREFIX);
|
||||
|
||||
|
@ -5,33 +5,44 @@ export interface OTBRInfo {
|
||||
border_agent_id: string;
|
||||
channel: number;
|
||||
extended_address: string;
|
||||
extended_pan_id: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
|
||||
export type OTBRInfoDict = Record<string, OTBRInfo>;
|
||||
|
||||
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfoDict> =>
|
||||
hass.callWS({
|
||||
type: "otbr/info",
|
||||
});
|
||||
|
||||
export const OTBRCreateNetwork = (hass: HomeAssistant): Promise<void> =>
|
||||
export const OTBRCreateNetwork = (
|
||||
hass: HomeAssistant,
|
||||
extended_address: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "otbr/create_network",
|
||||
extended_address,
|
||||
});
|
||||
|
||||
export const OTBRSetNetwork = (
|
||||
hass: HomeAssistant,
|
||||
extended_address: string,
|
||||
dataset_id: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "otbr/set_network",
|
||||
extended_address,
|
||||
dataset_id,
|
||||
});
|
||||
|
||||
export const OTBRSetChannel = (
|
||||
hass: HomeAssistant,
|
||||
extended_address: string,
|
||||
channel: number
|
||||
): Promise<{ delay: number }> =>
|
||||
hass.callWS({
|
||||
type: "otbr/set_channel",
|
||||
extended_address,
|
||||
channel,
|
||||
});
|
||||
|
@ -1,3 +1,7 @@
|
||||
import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface BasePerson {
|
||||
@ -18,6 +22,20 @@ export interface PersonMutableParams {
|
||||
picture: string | null;
|
||||
}
|
||||
|
||||
interface PersonEntityAttributes extends HassEntityAttributeBase {
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
device_trackers?: string[];
|
||||
editable?: boolean;
|
||||
gps_accuracy?: number;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}
|
||||
|
||||
export interface PersonEntity extends HassEntityBase {
|
||||
attributes: PersonEntityAttributes;
|
||||
}
|
||||
|
||||
export const fetchPersons = (hass: HomeAssistant) =>
|
||||
hass.callWS<{
|
||||
storage: Person[];
|
||||
|
@ -1,21 +1,27 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ThresholdPreview {
|
||||
const HAS_CUSTOM_PREVIEW = ["template"];
|
||||
|
||||
export interface GenericPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const subscribePreviewThreshold = (
|
||||
export const subscribePreviewGeneric = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: ThresholdPreview) => void
|
||||
callback: (preview: GenericPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "threshold/start_preview",
|
||||
type: `${domain}/start_preview`,
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
||||
|
||||
export const previewModule = (domain: string): string =>
|
||||
HAS_CUSTOM_PREVIEW.includes(domain) ? domain : "generic";
|
4
src/data/registry.ts
Normal file
4
src/data/registry.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface RegistryEntry {
|
||||
created_at: number;
|
||||
modified_at: number;
|
||||
}
|
@ -32,7 +32,11 @@ export const fetchRepairsIssues = (conn: Connection) =>
|
||||
type: "repairs/list_issues",
|
||||
});
|
||||
|
||||
export const fetchRepairsIssueData = (conn: Connection, domain, issue_id) =>
|
||||
export const fetchRepairsIssueData = (
|
||||
conn: Connection,
|
||||
domain: string,
|
||||
issue_id: string
|
||||
) =>
|
||||
conn.sendMessagePromise<{ issue_data: { string: any } }>({
|
||||
type: "repairs/get_issue_data",
|
||||
domain,
|
||||
|
@ -49,7 +49,7 @@ const targetStruct = object({
|
||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: optional(string()),
|
||||
action: optional(string()),
|
||||
service_template: optional(string()),
|
||||
entity_id: optional(string()),
|
||||
target: optional(targetStruct),
|
||||
@ -62,7 +62,7 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
const playMediaActionStruct: Describe<PlayMediaAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: literal("media_player.play_media"),
|
||||
action: literal("media_player.play_media"),
|
||||
target: optional(object({ entity_id: optional(string()) })),
|
||||
entity_id: optional(string()),
|
||||
data: object({ media_content_id: string(), media_content_type: string() }),
|
||||
@ -73,7 +73,7 @@ const playMediaActionStruct: Describe<PlayMediaAction> = assign(
|
||||
const activateSceneActionStruct: Describe<ServiceSceneAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
service: literal("scene.turn_on"),
|
||||
action: literal("scene.turn_on"),
|
||||
target: optional(object({ entity_id: optional(string()) })),
|
||||
entity_id: optional(string()),
|
||||
metadata: object(),
|
||||
@ -132,7 +132,7 @@ export interface EventAction extends BaseAction {
|
||||
}
|
||||
|
||||
export interface ServiceAction extends BaseAction {
|
||||
service?: string;
|
||||
action?: string;
|
||||
service_template?: string;
|
||||
entity_id?: string;
|
||||
target?: HassServiceTarget;
|
||||
@ -160,7 +160,7 @@ export interface DelayAction extends BaseAction {
|
||||
}
|
||||
|
||||
export interface ServiceSceneAction extends BaseAction {
|
||||
service: "scene.turn_on";
|
||||
action: "scene.turn_on";
|
||||
target?: { entity_id?: string };
|
||||
entity_id?: string;
|
||||
metadata: Record<string, unknown>;
|
||||
@ -191,7 +191,7 @@ export interface WaitForTriggerAction extends BaseAction {
|
||||
}
|
||||
|
||||
export interface PlayMediaAction extends BaseAction {
|
||||
service: "media_player.play_media";
|
||||
action: "media_player.play_media";
|
||||
target?: { entity_id?: string };
|
||||
entity_id?: string;
|
||||
data: { media_content_id: string; media_content_type: string };
|
||||
@ -404,7 +404,7 @@ export const getActionType = (action: Action): ActionType => {
|
||||
if ("set_conversation_response" in action) {
|
||||
return "set_conversation_response";
|
||||
}
|
||||
if ("service" in action) {
|
||||
if ("action" in action) {
|
||||
if ("metadata" in action) {
|
||||
if (is(action, activateSceneActionStruct)) {
|
||||
return "activate_scene";
|
||||
@ -425,3 +425,60 @@ export const hasScriptFields = (
|
||||
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
|
||||
return fields !== undefined && Object.keys(fields).length > 0;
|
||||
};
|
||||
|
||||
export const migrateAutomationAction = (
|
||||
action: Action | Action[]
|
||||
): Action | Action[] => {
|
||||
if (Array.isArray(action)) {
|
||||
return action.map(migrateAutomationAction) as Action[];
|
||||
}
|
||||
|
||||
if ("service" in action) {
|
||||
if (!("action" in action)) {
|
||||
action.action = action.service;
|
||||
}
|
||||
delete action.service;
|
||||
}
|
||||
|
||||
if ("sequence" in action) {
|
||||
for (const sequenceAction of (action as SequenceAction).sequence) {
|
||||
migrateAutomationAction(sequenceAction);
|
||||
}
|
||||
}
|
||||
|
||||
const actionType = getActionType(action);
|
||||
|
||||
if (actionType === "parallel") {
|
||||
const _action = action as ParallelAction;
|
||||
migrateAutomationAction(_action.parallel);
|
||||
}
|
||||
|
||||
if (actionType === "choose") {
|
||||
const _action = action as ChooseAction;
|
||||
if (Array.isArray(_action.choose)) {
|
||||
for (const choice of _action.choose) {
|
||||
migrateAutomationAction(choice.sequence);
|
||||
}
|
||||
} else if (_action.choose) {
|
||||
migrateAutomationAction(_action.choose.sequence);
|
||||
}
|
||||
if (_action.default) {
|
||||
migrateAutomationAction(_action.default);
|
||||
}
|
||||
}
|
||||
|
||||
if (actionType === "repeat") {
|
||||
const _action = action as RepeatAction;
|
||||
migrateAutomationAction(_action.repeat.sequence);
|
||||
}
|
||||
|
||||
if (actionType === "if") {
|
||||
const _action = action as IfAction;
|
||||
migrateAutomationAction(_action.then);
|
||||
if (_action.else) {
|
||||
migrateAutomationAction(_action.else);
|
||||
}
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
|
@ -192,7 +192,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
|
||||
if (
|
||||
config.service_template ||
|
||||
(config.service && isTemplate(config.service))
|
||||
(config.action && isTemplate(config.action))
|
||||
) {
|
||||
return hass.localize(
|
||||
targets.length
|
||||
@ -204,8 +204,8 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
);
|
||||
}
|
||||
|
||||
if (config.service) {
|
||||
const [domain, serviceName] = config.service.split(".", 2);
|
||||
if (config.action) {
|
||||
const [domain, serviceName] = config.action.split(".", 2);
|
||||
const service =
|
||||
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
|
||||
hass.services[domain][serviceName]?.name;
|
||||
@ -217,7 +217,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.service,
|
||||
name: service || config.action,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
@ -230,7 +230,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
{
|
||||
name: service
|
||||
? `${domainToName(hass.localize, domain)}: ${service}`
|
||||
: config.service,
|
||||
: config.action,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ export interface RelatedResult {
|
||||
device?: string[];
|
||||
entity?: string[];
|
||||
group?: string[];
|
||||
integration?: string[];
|
||||
scene?: string[];
|
||||
script?: string[];
|
||||
script_blueprint?: string[];
|
||||
|
@ -2,6 +2,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { UiAction } from "../panels/lovelace/components/hui-action-editor";
|
||||
import { HomeAssistant, ItemPath } from "../types";
|
||||
import {
|
||||
@ -13,8 +15,6 @@ import {
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { EntitySources } from "./entity_sources";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
@ -64,7 +64,8 @@ export type Selector =
|
||||
| TTSSelector
|
||||
| TTSVoiceSelector
|
||||
| UiActionSelector
|
||||
| UiColorSelector;
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
action: {
|
||||
@ -202,6 +203,7 @@ export interface LegacyDeviceSelector {
|
||||
export interface DurationSelector {
|
||||
duration: {
|
||||
enable_day?: boolean;
|
||||
enable_millisecond?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@ -455,6 +457,13 @@ export interface UiColorSelector {
|
||||
ui_color: { default_color?: boolean } | null;
|
||||
}
|
||||
|
||||
export interface UiStateContentSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ui_state_content: {
|
||||
entity_id?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const expandLabelTarget = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface TimeDatePreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const subscribePreviewTimeDate = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: TimeDatePreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "time_date/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
@ -92,7 +92,7 @@ export const computeDisplayTimer = (
|
||||
return hass.formatEntityState(stateObj);
|
||||
}
|
||||
|
||||
let display = secondsToDuration(timeRemaining || 0);
|
||||
let display = secondsToDuration(timeRemaining || 0) || "0";
|
||||
|
||||
if (stateObj.state === "paused") {
|
||||
display = `${display} (${hass.formatEntityState(stateObj)})`;
|
||||
|
@ -2,23 +2,22 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
ThresholdPreview,
|
||||
subscribePreviewThreshold,
|
||||
} from "../../../data/threshold";
|
||||
import { GenericPreview, subscribePreviewGeneric } from "../../../data/preview";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("flow-preview-threshold")
|
||||
class FlowPreviewThreshold extends LitElement {
|
||||
@customElement("flow-preview-generic")
|
||||
class FlowPreviewGeneric extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public domain!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
@ -55,7 +54,7 @@ class FlowPreviewThreshold extends LitElement {
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: ThresholdPreview) => {
|
||||
private _setPreview = (preview: GenericPreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
@ -79,14 +78,14 @@ class FlowPreviewThreshold extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewThreshold(
|
||||
this._unsub = subscribePreviewGeneric(
|
||||
this.hass,
|
||||
this.domain,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||
} catch (err: any) {
|
||||
if (typeof err.message === "string") {
|
||||
@ -103,6 +102,6 @@ class FlowPreviewThreshold extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-threshold": FlowPreviewThreshold;
|
||||
"flow-preview-generic": FlowPreviewGeneric;
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import { GroupPreview, subscribePreviewGroup } from "../../../data/group";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
|
||||
@customElement("flow-preview-group")
|
||||
class FlowPreviewGroup extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property({ attribute: false }) public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: GroupPreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
};
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewGroup(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
} catch (err) {
|
||||
this._preview = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-group": FlowPreviewGroup;
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
TimeDatePreview,
|
||||
subscribePreviewTimeDate,
|
||||
} from "../../../data/time_date";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
|
||||
@customElement("flow-preview-time_date")
|
||||
class FlowPreviewTimeDate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property() public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: TimeDatePreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
};
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewTimeDate(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
} catch (err) {
|
||||
this._preview = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-time_date": FlowPreviewTimeDate;
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { previewModule } from "../../data/preview";
|
||||
|
||||
@customElement("step-flow-form")
|
||||
class StepFlowForm extends LitElement {
|
||||
@ -76,8 +77,9 @@ class StepFlowForm extends LitElement {
|
||||
"ui.panel.config.integrations.config_flow.preview"
|
||||
)}:
|
||||
</h3>
|
||||
${dynamicElement(`flow-preview-${this.step.preview}`, {
|
||||
${dynamicElement(`flow-preview-${previewModule(step.preview)}`, {
|
||||
hass: this.hass,
|
||||
domain: step.preview,
|
||||
flowType: this.flowConfig.flowType,
|
||||
handler: step.handler,
|
||||
stepId: step.step_id,
|
||||
@ -120,7 +122,7 @@ class StepFlowForm extends LitElement {
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("step") && this.step?.preview) {
|
||||
import(`./previews/flow-preview-${this.step.preview}`);
|
||||
import(`./previews/flow-preview-${previewModule(this.step.preview)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,14 @@ import {
|
||||
mdiTuneVariant,
|
||||
mdiWaterPercent,
|
||||
} from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
@ -39,6 +46,17 @@ class MoreInfoClimate extends LitElement {
|
||||
|
||||
@state() private _mainControl: MainControl = "temperature";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
changedProps.has("stateObj") &&
|
||||
this.stateObj &&
|
||||
this._mainControl === "humidity" &&
|
||||
!supportsFeature(this.stateObj, ClimateEntityFeature.TARGET_HUMIDITY)
|
||||
) {
|
||||
this._mainControl = "temperature";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user