Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
ebe5207b6e Improve datatable 2024-06-25 17:05:51 +02:00
262 changed files with 4588 additions and 8547 deletions

View File

@@ -37,9 +37,3 @@ not dead
unreleased versions unreleased versions
last 7 years 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

View File

@@ -8,7 +8,6 @@
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev", "postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap", "postStartCommand": "script/bootstrap",
"containerEnv": { "containerEnv": {
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}" "WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
}, },
"customizations": { "customizations": {

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -89,7 +89,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.3
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -113,7 +113,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.3
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.3
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.3
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.3 uses: actions/setup-node@v4.0.2
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.07.1 uses: home-assistant/wheels@2024.01.0
with: with:
abi: cp311 abi: cp311
tag: musllinux_1_2 tag: musllinux_1_2

View File

@@ -1,55 +0,0 @@
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}`;
}

View File

@@ -47,7 +47,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__DEV__: !isProdBuild, __DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()), __VERSION__: JSON.stringify(env.version()),
__DEMO__: false, __DEMO__: false,
__SUPERVISOR__: false, __SUPERVISOR__: false,
@@ -79,12 +79,7 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
sourceMap: !isTestBuild, sourceMap: !isTestBuild,
}); });
module.exports.babelOptions = ({ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
latestBuild,
isProdBuild,
isTestBuild,
sw,
}) => ({
babelrc: false, babelrc: false,
compact: false, compact: false,
assumptions: { assumptions: {
@@ -92,7 +87,7 @@ module.exports.babelOptions = ({
setPublicClassFields: true, setPublicClassFields: true,
setSpreadProperties: true, setSpreadProperties: true,
}, },
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`, browserslistEnv: latestBuild ? "modern" : "legacy",
presets: [ presets: [
[ [
"@babel/preset-env", "@babel/preset-env",
@@ -220,13 +215,7 @@ module.exports.config = {
return { return {
name: "frontend" + nameSuffix(latestBuild), name: "frontend" + nameSuffix(latestBuild),
entry: { entry: {
"service-worker": service_worker: "./src/entrypoints/service_worker.ts",
!env.useRollup() && !latestBuild
? {
import: "./src/entrypoints/service-worker.ts",
layer: "sw",
}
: "./src/entrypoints/service-worker.ts",
app: "./src/entrypoints/app.ts", app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts", authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts", onboarding: "./src/entrypoints/onboarding.ts",

View File

@@ -32,7 +32,4 @@ module.exports = {
} }
return version[1]; return version[1];
}, },
isDevContainer() {
return process.env.DEV_CONTAINER === "1";
},
}; };

View File

@@ -1,19 +1,20 @@
// Generate service workers // Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
import { deleteAsync } from "del"; import fs from "fs-extra";
import gulp from "gulp"; import gulp from "gulp";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "path";
import { join, relative } from "node:path"; import sourceMapUrl from "source-map-url";
import { injectManifest } from "workbox-build"; import workboxBuild from "workbox-build";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
const SW_MAP = { const swDest = path.resolve(paths.app_output_root, "service_worker.js");
[paths.app_output_latest]: "modern",
[paths.app_output_es5]: "legacy",
};
const SW_DEV = const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
`
gulp.task("gen-service-worker-app-dev", (done) => {
writeSW(
`
console.debug('Service worker disabled in development'); console.debug('Service worker disabled in development');
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
@@ -21,61 +22,72 @@ self.addEventListener('install', (event) => {
// removing any prod service worker the dev might have running // removing any prod service worker the dev might have running
self.skipWaiting(); 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", () => gulp.task("gen-service-worker-app-prod", async () => {
Promise.all( // Read bundled source file
Object.entries(SW_MAP).map(async ([outPath, build]) => { const bundleManifestLatest = fs.readJsonSync(
const manifest = JSON.parse( path.resolve(paths.app_output_latest, "manifest.json")
await readFile(join(outPath, "manifest.json"), "utf-8") );
); let serviceWorkerContent = fs.readFileSync(
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]); paths.app_output_root + bundleManifestLatest["service_worker.js"],
const buildDir = relative(paths.app_output_root, outPath); "utf-8"
const { warnings } = await injectManifest({ );
swSrc,
swDest: join(paths.app_output_root, `sw-${build}.js`), // Delete old file from frontend_latest so manifest won't pick it up
injectionPoint: "__WB_MANIFEST__", fs.removeSync(
// Files that mach this pattern will be considered unique and skip revision check paths.app_output_root + bundleManifestLatest["service_worker.js"]
// ignore JS files + translation files );
dontCacheBustURLsMatching: new RegExp( fs.removeSync(
`(?:${buildDir}/.+|static/translations/.+)` paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
), );
globDirectory: paths.app_output_root,
globPatterns: [ // Remove ES5
`${buildDir}/*.js`, const bundleManifestES5 = fs.readJsonSync(
// Cache all English translations because we catch them as fallback path.resolve(paths.app_output_es5, "manifest.json")
// 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' fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
"static/translations/**/en-+([a-fv0-9]).json", fs.removeSync(
// Icon shown on splash screen paths.app_output_root + bundleManifestES5["service_worker.js.map"]
"static/icons/favicon-192x192.png", );
"static/icons/favicon.ico",
// Common fonts const workboxManifest = await workboxBuild.getManifest({
"static/fonts/roboto/Roboto-Light.woff2", // Files that mach this pattern will be considered unique and skip revision check
"static/fonts/roboto/Roboto-Medium.woff2", // ignore JS files + translation files
"static/fonts/roboto/Roboto-Regular.woff2", dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
"static/fonts/roboto/Roboto-Bold.woff2",
], globDirectory: paths.app_output_root,
globIgnores: [`${buildDir}/service-worker*`], globPatterns: [
}); "frontend_latest/*.js",
if (warnings.length > 0) { // Cache all English translations because we catch them as fallback
console.warn( // Using pattern to match hash instead of * to avoid caching en-GB
`Problems while injecting ${build} service worker:\n`, // 'v' added as valid hash letter because in dev we hash with 'dev'
warnings.join("\n") "static/translations/**/en-+([a-fv0-9]).json",
); // Icon shown on splash screen
} "static/icons/favicon-192x192.png",
await deleteAsync(`${swSrc}?(.map)`); "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);
});

View File

@@ -244,11 +244,11 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved. // TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated // Will be OK for now as long as we don't have anything more complicated
// than a base translation + region. // than a base translation + region.
const masterStream = gulp gulp
.src(`${workDir}/en.json`) .src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true })); .pipe(new PassThrough({ objectMode: true }))
masterStream.pipe(hashStream, { end: false }); .pipe(hashStream, { end: false });
const mergesFinished = [finished(masterStream)]; const mergesFinished = [];
for (const translationFile of translationFiles) { for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json"); const locale = basename(translationFile, ".json");
const subtags = locale.split("-"); const subtags = locale.split("-");

View File

@@ -40,12 +40,8 @@ const runDevServer = async ({
compiler, compiler,
contentBase, contentBase,
port, port,
listenHost = undefined, listenHost = "localhost",
}) => { }) => {
if (listenHost === undefined) {
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new WebpackDevServer( const server = new WebpackDevServer(
{ {
hot: false, hot: false,

View File

@@ -63,25 +63,17 @@ const createWebpackConfig = ({
rules: [ rules: [
{ {
test: /\.m?js$|\.ts$/, test: /\.m?js$|\.ts$/,
use: (info) => ({ use: {
loader: "babel-loader", loader: "babel-loader",
options: { options: {
...bundle.babelOptions({ ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }),
latestBuild,
isProdBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
cacheDirectory: !isProdBuild, cacheDirectory: !isProdBuild,
cacheCompression: false, cacheCompression: false,
}, },
}), },
resolve: { resolve: {
fullySpecified: false, fullySpecified: false,
}, },
parser: {
worker: ["*context.audioWorklet.addModule()", "..."],
},
}, },
{ {
test: /\.css$/, test: /\.css$/,
@@ -100,15 +92,11 @@ const createWebpackConfig = ({
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named", chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
splitChunks: { splitChunks: {
// Disable splitting for web workers and worklets because imports of // Disable splitting for web workers with ESM output
// external chunks are broken for: // Imports of external chunks are broken
// - ESM output: https://github.com/webpack/webpack/issues/17014 chunks: latestBuild
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543 ? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
chunks: (chunk) => : undefined,
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
}, },
}, },
plugins: [ plugins: [
@@ -240,7 +228,6 @@ const createWebpackConfig = ({
), ),
}, },
experiments: { experiments: {
layers: true,
outputModule: true, outputModule: true,
}, },
}; };

View File

@@ -232,5 +232,17 @@ http:
</p> </p>
</div> </div>
</hc-layout> </hc-layout>
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body> </body>
</html> </html>

View File

@@ -14,6 +14,12 @@
--background-color: #41bdf5; --background-color: #41bdf5;
} }
</style> </style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head> </head>
<body> <body>
<%= renderTemplate("../../../src/html/_js_base.html.template") %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>

View File

@@ -11,4 +11,10 @@
font-size: initial; font-size: initial;
} }
</style> </style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</html> </html>

View File

@@ -1,3 +1,4 @@
import "../../../src/resources/safari-14-attachshadow-patch";
import "./layout/hc-connect"; import "./layout/hc-connect";
import("../../../src/resources/ha-style"); import("../../../src/resources/ha-style");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,7 +1,7 @@
import { convertEntities } from "../../../../src/fake_data/entity"; import { convertEntities } from "../../../../src/fake_data/entity";
import { DemoConfig } from "../types"; import { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = (localize) => export const demoEntitiesSections: DemoConfig["entities"] = () =>
convertEntities({ convertEntities({
"cover.living_room_garden_shutter": { "cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter", entity_id: "cover.living_room_garden_shutter",
@@ -113,30 +113,11 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
}, },
"media_player.living_room_nest_mini": { "media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini", entity_id: "media_player.living_room_nest_mini",
state: "on", state: "off",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, friendly_name: "Living room Nest Mini",
is_volume_muted: false, supported_features: 152461,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.living_room_nest_mini"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"cover.kitchen_shutter": { "cover.kitchen_shutter": {
@@ -187,27 +168,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
state: "on", state: "on",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, friendly_name: "Kitchen Nest Audio",
is_volume_muted: false, supported_features: 152461,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.kitchen_nest_audio"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"binary_sensor.tesla_wall_connector_vehicle_connected": { "binary_sensor.tesla_wall_connector_vehicle_connected": {
@@ -371,28 +333,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
entity_id: "media_player.study_nest_hub", entity_id: "media_player.study_nest_hub",
state: "off", state: "off",
attributes: { attributes: {
device_class: "speaker", friendly_name: "Study Nest Hub",
volume_level: 0.18, supported_features: 152461,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.study_nest_hub"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
}, },
}, },
"sensor.standing_desk_height": { "sensor.standing_desk_height": {

View File

@@ -1,25 +1,40 @@
import { isFrontpageEmbed } from "../../util/is_frontpage";
import { DemoConfig } from "../types"; import { DemoConfig } from "../types";
export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
title: "Home Assistant Demo", title: "Home Assistant Demo",
views: [ views: [
{ {
type: "sections", type: "sections",
title: isFrontpageEmbed ? "Home Assistant" : "Demo", title: "Demo",
path: "home", path: "home",
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
sections: [ sections: [
...(isFrontpageEmbed {
? [] title: "Welcome 👋",
: [ cards: [{ type: "custom:ha-demo-card" }],
{ },
title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
cards: [{ type: "custom:ha-demo-card" }],
},
]),
{ {
cards: [ cards: [
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Garden",
},
{
type: "tile",
entity: "cover.living_room_graveyard_shutter",
name: "Rear",
},
{
type: "tile",
entity: "cover.living_room_left_shutter",
name: "Left",
},
{
type: "tile",
entity: "cover.living_room_right_shutter",
name: "Right",
},
{ {
type: "tile", type: "tile",
entity: "light.floor_lamp", entity: "light.floor_lamp",
@@ -45,17 +60,13 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
detail: 1, detail: 1,
name: "Temperature", name: "Temperature",
}, },
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Blinds",
},
{ {
type: "tile", type: "tile",
entity: "media_player.living_room_nest_mini", entity: "media_player.living_room_nest_mini",
name: "Nest Mini",
}, },
], ],
title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `, title: "🛋️ Living room ",
}, },
{ {
type: "grid", type: "grid",
@@ -88,9 +99,10 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{ {
type: "tile", type: "tile",
entity: "media_player.kitchen_nest_audio", entity: "media_player.kitchen_nest_audio",
name: "Nest Audio",
}, },
], ],
title: `👩‍🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`, title: "👩‍🍳 Kitchen",
}, },
{ {
type: "grid", type: "grid",
@@ -132,7 +144,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey", color: "dark-grey",
}, },
], ],
title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`, title: "⚡️ Energy",
}, },
{ {
type: "grid", type: "grid",
@@ -169,7 +181,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"], state_content: ["preset_mode", "current_temperature"],
}, },
], ],
title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`, title: "🌤️ Climate",
}, },
{ {
type: "grid", type: "grid",
@@ -187,6 +199,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{ {
type: "tile", type: "tile",
entity: "media_player.study_nest_hub", entity: "media_player.study_nest_hub",
name: "Nest Hub",
}, },
{ {
type: "tile", type: "tile",
@@ -196,7 +209,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:desk", icon: "mdi:desk",
}, },
], ],
title: `🧑‍💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`, title: "🧑‍💻 Study",
}, },
{ {
type: "grid", type: "grid",
@@ -230,7 +243,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance", name: "Illuminance",
}, },
], ],
title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`, title: "🌳 Outdoor",
}, },
{ {
type: "grid", type: "grid",
@@ -260,7 +273,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant", icon: "mdi:home-assistant",
}, },
], ],
title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`, title: "🎉 Updates",
}, },
], ],
}, },

View File

@@ -1,4 +1,4 @@
import "./util/is_frontpage"; import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo"; import "./ha-demo";
import("../../src/resources/ha-style"); import("../../src/resources/ha-style");

View File

@@ -93,5 +93,16 @@
} }
</script> </script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %> <%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body> </body>
</html> </html>

View File

@@ -1,55 +1,5 @@
import { convertEntities } from "../../../src/fake_data/entity"; import { convertEntities } from "../../../src/fake_data/entity";
export const mapEntities = () =>
convertEntities({
"zone.home": {
entity_id: "zone.home",
state: "zoning",
attributes: {
hidden: true,
latitude: 52.3631339,
longitude: 4.8903147,
radius: 200,
friendly_name: "Home",
icon: "hademo:home",
},
},
"zone.uva": {
entity_id: "zone.buckhead",
state: "zoning",
attributes: {
hidden: true,
radius: 400,
friendly_name: "UvA",
icon: "hademo:school",
latitude: 52.3558182,
longitude: 4.9535376,
},
},
"person.arsaboo": {
entity_id: "person.arsaboo",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Arsaboo",
latitude: 52.3579946,
longitude: 4.8664597,
entity_picture: "/assets/arsaboo/images/arsaboo.jpg",
},
},
"person.melody": {
entity_id: "person.melody",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Melody",
latitude: 52.3408927,
longitude: 4.8711073,
entity_picture: "/assets/arsaboo/images/melody.jpg",
},
},
});
export const energyEntities = () => export const energyEntities = () =>
convertEntities({ convertEntities({
"sensor.grid_fossil_fuel_percentage": { "sensor.grid_fossil_fuel_percentage": {

View File

@@ -7,25 +7,16 @@ import {
} from "../configs/demo-configs"; } from "../configs/demo-configs";
import "../custom-cards/cast-demo-row"; import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card"; import "../custom-cards/ha-demo-card";
import { mapEntities } from "./entities";
export const mockLovelace = ( export const mockLovelace = (
hass: MockHomeAssistant, hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc> localizePromise: Promise<LocalizeFunc>
) => { ) => {
hass.mockWS("lovelace/config", ({ url_path }) => { hass.mockWS("lovelace/config", () =>
if (url_path === "map") { Promise.all([selectedDemoConfig, localizePromise]).then(
hass.addEntities(mapEntities());
return {
strategy: {
type: "map",
},
};
}
return Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize) ([config, localize]) => config.lovelace(localize)
); )
}); );
hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([])); hass.mockWS("lovelace/resources", () => Promise.resolve([]));

View File

@@ -1 +0,0 @@
export const isFrontpageEmbed = document.location.search === "?frontpage";

View File

@@ -3,16 +3,13 @@ title: When to use remove, delete, add and create
subtitle: The difference between remove/delete and add/create. subtitle: The difference between remove/delete and add/create.
--- ---
# Removing or deleting content # Remove vs Delete
_Remove_ and _Delete_ are quite similar, but can be frustrating if used inconsistently. Remove and Delete are quite similar, but can be frustrating if used inconsistently.
- Remove refers to an action that can be restored or reapplied.
- Delete refers to a permanent, non-recoverable action.
## Remove ## Remove
The term _Remove_ should always be used when an item/setting or content is to be removed or disassociated, but the action can be reversed or reapplied. Take away and set aside, but kept in existence.
For example: For example:
@@ -25,7 +22,7 @@ For example:
## Delete ## Delete
The term _Delete_ should always be used to refer to any action that will cause the permanent deletion of an item/setting or content. Erase, rendered nonexistent or nonrecoverable.
For example: For example:

View File

@@ -53,7 +53,6 @@ const DEVICES = [
identifiers: [["demo", "volume1"] as [string, string]], identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Dishwasher", name: "Dishwasher",
sw_version: null, sw_version: null,
@@ -73,7 +72,6 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Lamp", name: "Lamp",
sw_version: null, sw_version: null,
@@ -93,7 +91,6 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: "User name", name_by_user: "User name",
name: "Technical name", name: "Technical name",
sw_version: null, sw_version: null,

View File

@@ -53,7 +53,6 @@ const DEVICES = [
identifiers: [["demo", "volume1"] as [string, string]], identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Dishwasher", name: "Dishwasher",
sw_version: null, sw_version: null,
@@ -73,7 +72,6 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: null, name_by_user: null,
name: "Lamp", name: "Lamp",
sw_version: null, sw_version: null,
@@ -93,7 +91,6 @@ const DEVICES = [
identifiers: [["demo", "pwm1"] as [string, string]], identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null, manufacturer: null,
model: null, model: null,
model_id: null,
name_by_user: "User name", name_by_user: "User name",
name: "Technical name", name: "Technical name",
sw_version: null, sw_version: null,

View File

@@ -140,9 +140,6 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_preheating", "auto", undefined, { createEntity("climate.auto_preheating", "auto", undefined, {
hvac_action: "preheating", hvac_action: "preheating",
}), }),
createEntity("climate.auto_defrosting", "auto", undefined, {
hvac_action: "defrosting",
}),
createEntity("climate.auto_heating", "auto", undefined, { createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating", hvac_action: "heating",
}), }),

View File

@@ -215,7 +215,6 @@ const createDeviceRegistryEntries = (
connections: [], connections: [],
manufacturer: "ESPHome", manufacturer: "ESPHome",
model: "Mock Device", model: "Mock Device",
model_id: "ABC-001",
name: "Tag Reader", name: "Tag Reader",
sw_version: null, sw_version: null,
hw_version: "1.0.0", hw_version: "1.0.0",

View File

@@ -1,8 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics"; import type { IFuseOptions } from "fuse.js";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) { export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = { const options: IFuseOptions<StoreAddon> = {
@@ -10,8 +8,7 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: getStripDiacriticsFn,
}; };
const fuse = new Fuse(addons, options); const fuse = new Fuse(addons, options);
return fuse.search(stripDiacritics(filter)).map((result) => result.item); return fuse.search(filter).map((result) => result.item);
} }

View File

@@ -1,5 +1,6 @@
// Compat needs to be first import // Compat needs to be first import
import "../../src/resources/compatibility"; import "../../src/resources/compatibility";
import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main"; import "./hassio-main";
import("../../src/resources/ha-style"); import("../../src/resources/ha-style");

View File

@@ -25,15 +25,15 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.24.8", "@babel/runtime": "7.24.7",
"@braintree/sanitize-url": "7.0.4", "@braintree/sanitize-url": "7.0.3",
"@codemirror/autocomplete": "6.17.0", "@codemirror/autocomplete": "6.16.3",
"@codemirror/commands": "6.6.0", "@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2", "@codemirror/language": "6.10.2",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.28.4", "@codemirror/view": "6.28.2",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8", "@formatjs/intl-displaynames": "6.6.8",
@@ -43,12 +43,12 @@
"@formatjs/intl-numberformat": "8.10.3", "@formatjs/intl-numberformat": "8.10.3",
"@formatjs/intl-pluralrules": "5.2.14", "@formatjs/intl-pluralrules": "5.2.14",
"@formatjs/intl-relativetimeformat": "11.2.14", "@formatjs/intl-relativetimeformat": "11.2.14",
"@fullcalendar/core": "6.1.15", "@fullcalendar/core": "6.1.11",
"@fullcalendar/daygrid": "6.1.15", "@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/interaction": "6.1.15", "@fullcalendar/interaction": "6.1.11",
"@fullcalendar/list": "6.1.15", "@fullcalendar/list": "6.1.11",
"@fullcalendar/luxon3": "6.1.15", "@fullcalendar/luxon3": "6.1.11",
"@fullcalendar/timegrid": "6.1.15", "@fullcalendar/timegrid": "6.1.11",
"@lezer/highlight": "1.2.0", "@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7", "@lit-labs/motion": "1.0.7",
@@ -80,7 +80,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "1.5.1", "@material/web": "1.5.0",
"@mdi/js": "7.4.47", "@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
@@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.3", "@vaadin/combo-box": "24.4.0",
"@vaadin/vaadin-themable-mixin": "24.4.3", "@vaadin/vaadin-themable-mixin": "24.4.0",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -118,7 +118,7 @@
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.4", "luxon": "3.4.4",
"marked": "13.0.2", "marked": "12.0.2",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -129,7 +129,7 @@
"rrule": "2.8.1", "rrule": "2.8.1",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"stacktrace-js": "2.0.2", "stacktrace-js": "2.0.2",
"superstruct": "2.0.2", "superstruct": "1.0.4",
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"tsparticles-engine": "2.12.0", "tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0", "tsparticles-preset-links": "2.12.0",
@@ -149,15 +149,15 @@
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.9", "@babel/core": "7.24.7",
"@babel/helper-define-polyfill-provider": "0.6.2", "@babel/helper-define-polyfill-provider": "0.6.2",
"@babel/plugin-proposal-decorators": "7.24.7", "@babel/plugin-proposal-decorators": "7.24.7",
"@babel/plugin-transform-runtime": "7.24.7", "@babel/plugin-transform-runtime": "7.24.7",
"@babel/preset-env": "7.24.8", "@babel/preset-env": "7.24.7",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.13.3", "@bundle-stats/plugin-webpack-filter": "4.13.2",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.6.0", "@lokalise/node-api": "12.5.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1", "@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "21.0.0", "@octokit/rest": "21.0.0",
@@ -168,7 +168,7 @@
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7", "@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.16", "@types/chromecast-caf-receiver": "6.0.15",
"@types/chromecast-caf-sender": "1.0.10", "@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4", "@types/color-name": "1.1.4",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
@@ -178,15 +178,15 @@
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.7", "@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8", "@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.16.1", "@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.16.1", "@typescript-eslint/parser": "7.13.1",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
@@ -200,16 +200,16 @@
"eslint-import-resolver-webpack": "0.13.8", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.14.0", "eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit-a11y": "4.1.4", "eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "4.0.0", "eslint-plugin-unused-imports": "4.0.0",
"eslint-plugin-wc": "2.1.0", "eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "11.0.0", "glob": "10.4.2",
"gulp": "5.0.0", "gulp": "5.0.0",
"gulp-json-transform": "0.5.0", "gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2", "gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.0.11", "husky": "9.0.11",
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
@@ -220,30 +220,31 @@
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.10", "magic-string": "0.30.10",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.5.0", "mocha": "10.4.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.3.3", "prettier": "3.3.2",
"rollup": "2.79.1", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "18.0.0", "sinon": "18.0.0",
"source-map-url": "0.4.1",
"systemjs": "6.15.1", "systemjs": "6.15.1",
"tar": "7.4.0", "tar": "7.4.0",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.5.3", "typescript": "5.4.5",
"webpack": "5.93.0", "webpack": "5.92.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4", "webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1", "webpackbar": "6.0.1",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" "workbox-build": "7.1.1"
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
@@ -252,7 +253,7 @@
"lit": "2.8.0", "lit": "2.8.0",
"clean-css": "5.3.3", "clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3", "@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15", "@fullcalendar/daygrid": "6.1.11",
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "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" "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 430 B

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240719.0" version = "20240610.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -125,7 +125,6 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"off", "off",
"idle", "idle",
"preheating", "preheating",
"defrosting",
"heating", "heating",
"cooling", "cooling",
"drying", "drying",

View File

@@ -25,9 +25,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
if (__DEMO__) { if (__DEMO__) {
if (replace) { if (replace) {
mainWindow.history.replaceState( mainWindow.history.replaceState(
mainWindow.history.state?.root mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
? { root: true }
: (options?.data ?? null),
"", "",
`${mainWindow.location.pathname}#${path}` `${mainWindow.location.pathname}#${path}`
); );
@@ -36,7 +34,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
} }
} else if (replace) { } else if (replace) {
mainWindow.history.replaceState( mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null), mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
"", "",
path path
); );

View File

@@ -1,4 +1,3 @@
import { stripDiacritics } from "../strip-diacritics";
import { fuzzyScore } from "./filter"; import { fuzzyScore } from "./filter";
/** /**
@@ -20,10 +19,10 @@ export const fuzzySequentialMatch = (
for (const word of item.strings) { for (const word of item.strings) {
const scores = fuzzyScore( const scores = fuzzyScore(
filter, filter,
stripDiacritics(filter.toLowerCase()), filter.toLowerCase(),
0, 0,
word, word,
stripDiacritics(word.toLowerCase()), word.toLowerCase(),
0, 0,
true true
); );

View File

@@ -1,2 +0,0 @@
export const stripDiacritics = (str: string) =>
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");

View File

@@ -159,10 +159,10 @@ export class StateHistoryChartTimeline extends LitElement {
}, },
afterUpdate: (y) => { afterUpdate: (y) => {
const yWidth = this.showNames const yWidth = this.showNames
? (y.width ?? 0) ? y.width ?? 0
: computeRTL(this.hass) : computeRTL(this.hass)
? 0 ? 0
: (y.left ?? 0); : y.left ?? 0;
if ( if (
this._yWidth !== Math.floor(yWidth) && this._yWidth !== Math.floor(yWidth) &&
y.ticks.length === this.data.length y.ticks.length === this.data.length

View File

@@ -1,319 +0,0 @@
import "@material/mwc-list";
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog";
import "../ha-list-item";
import "../ha-sortable";
import "../ha-button";
import { DataTableColumnContainer, DataTableColumnData } from "./ha-data-table";
import { DataTableSettingsDialogParams } from "./show-dialog-data-table-settings";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("dialog-data-table-settings")
export class DialogDataTableSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataTableSettingsDialogParams;
@state() private _columnOrder?: string[];
@state() private _hiddenColumns?: string[];
public showDialog(params: DataTableSettingsDialogParams) {
this._params = params;
this._columnOrder = params.columnOrder;
this._hiddenColumns = params.hiddenColumns;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _sortedColumns = memoizeOne(
(
columns: DataTableColumnContainer,
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) =>
Object.keys(columns)
.filter((col) => !columns[col].hidden)
.sort((a, b) => {
const orderA = columnOrder?.indexOf(a) ?? -1;
const orderB = columnOrder?.indexOf(b) ?? -1;
const hiddenA =
hiddenColumns?.includes(a) ?? Boolean(columns[a].defaultHidden);
const hiddenB =
hiddenColumns?.includes(b) ?? Boolean(columns[b].defaultHidden);
if (hiddenA !== hiddenB) {
return hiddenA ? 1 : -1;
}
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce(
(arr, key) => {
arr.push({ key, ...columns[key] });
return arr;
},
[] as (DataTableColumnData & { key: string })[]
)
);
protected render() {
if (!this._params) {
return nothing;
}
const localize = this._params.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
localize("ui.components.data-table.settings.header")
)}
>
<ha-sortable
@item-moved=${this._columnMoved}
draggable-selector=".draggable"
handle-selector=".handle"
>
<mwc-list>
${repeat(
columns,
(col) => col.key,
(col, _idx) => {
const canMove = !col.main && col.moveable !== false;
const canHide = !col.main && col.hideable !== false;
const isVisible = !(this._columnOrder &&
this._columnOrder.includes(col.key)
? (this._hiddenColumns?.includes(col.key) ??
col.defaultHidden)
: col.defaultHidden);
return html`<ha-list-item
hasMeta
class=${classMap({
hidden: !isVisible,
draggable: canMove && isVisible,
})}
graphic="icon"
noninteractive
>${col.title || col.label || col.key}
${canMove && isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
slot="graphic"
></ha-svg-icon>`
: nothing}
<ha-icon-button
tabindex="0"
class="action"
.disabled=${!canHide}
.hidden=${!isVisible}
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="meta"
.label=${this.hass!.localize(
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
{ title: typeof col.title === "string" ? col.title : "" }
)}
.column=${col.key}
@click=${this._toggle}
></ha-icon-button>
</ha-list-item>`;
}
)}
</mwc-list>
</ha-sortable>
<ha-button slot="secondaryAction" @click=${this._reset}
>${localize("ui.components.data-table.settings.restore")}</ha-button
>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${localize("ui.components.data-table.settings.done")}
</ha-button>
</ha-dialog>
`;
}
private _columnMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._params) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
const columnOrder = columns.map((column) => column.key);
const option = columnOrder.splice(oldIndex, 1)[0];
columnOrder.splice(newIndex, 0, option);
this._columnOrder = columnOrder;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_toggle(ev) {
if (!this._params) {
return;
}
const column = ev.target.column;
const wasHidden = ev.target.hidden;
const hidden = [
...(this._hiddenColumns ??
Object.entries(this._params.columns)
.filter(([_key, col]) => col.defaultHidden)
.map(([key]) => key)),
];
if (wasHidden && hidden.includes(column)) {
hidden.splice(hidden.indexOf(column), 1);
} else if (!wasHidden) {
hidden.push(column);
}
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
hidden
);
if (!this._columnOrder) {
this._columnOrder = columns.map((col) => col.key);
} else {
const newOrder = this._columnOrder.filter((col) => col !== column);
// Array.findLastIndex when supported or core-js polyfill
const findLastIndex = (
arr: Array<any>,
fn: (item: any, index: number, arr: Array<any>) => boolean
) => {
for (let i = arr.length - 1; i >= 0; i--) {
if (fn(arr[i], i, arr)) return i;
}
return -1;
};
let lastMoveable = findLastIndex(
newOrder,
(col) =>
col !== column &&
!hidden.includes(col) &&
!this._params!.columns[col].main &&
this._params!.columns[col].moveable !== false
);
if (lastMoveable === -1) {
lastMoveable = newOrder.length - 1;
}
columns.forEach((col) => {
if (!newOrder.includes(col.key)) {
if (col.moveable === false) {
newOrder.unshift(col.key);
} else {
newOrder.splice(lastMoveable + 1, 0, col.key);
}
if (
col.key !== column &&
col.defaultHidden &&
!hidden.includes(col.key)
) {
hidden.push(col.key);
}
}
});
this._columnOrder = newOrder;
}
this._hiddenColumns = hidden;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_reset() {
this._columnOrder = undefined;
this._hiddenColumns = undefined;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
--dialog-content-padding: 0 8px;
}
@media all and (max-width: 451px) {
ha-dialog {
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 250px;
--ha-dialog-border-radius: 28px 28px 0 0;
--mdc-dialog-min-height: calc(100% - 250px);
--mdc-dialog-max-height: calc(100% - 250px);
}
}
ha-list-item {
--mdc-list-side-padding: 12px;
overflow: visible;
}
.hidden {
color: var(--disabled-text-color);
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.actions {
display: flex;
flex-direction: row;
}
ha-icon-button {
display: block;
margin: -12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-data-table-settings": DialogDataTableSettings;
}
}

View File

@@ -34,7 +34,6 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "../search-input"; import "../search-input";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import { LocalizeFunc } from "../../common/translations/localize";
export interface RowClickedEvent { export interface RowClickedEvent {
id: string; id: string;
@@ -66,10 +65,6 @@ export interface DataTableSortColumnData {
valueColumn?: string; valueColumn?: string;
direction?: SortingDirection; direction?: SortingDirection;
groupable?: boolean; groupable?: boolean;
moveable?: boolean;
hideable?: boolean;
defaultHidden?: boolean;
showNarrow?: boolean;
} }
export interface DataTableColumnData<T = any> extends DataTableSortColumnData { export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
@@ -84,7 +79,6 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
| "overflow-menu" | "overflow-menu"
| "flex"; | "flex";
template?: (row: T) => TemplateResult | string | typeof nothing; template?: (row: T) => TemplateResult | string | typeof nothing;
extraTemplate?: (row: T) => TemplateResult | string | typeof nothing;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;
@@ -111,10 +105,6 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
export class HaDataTable extends LitElement { export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localizeFunc?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
@property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Array }) public data: DataTableRowData[] = [];
@@ -155,10 +145,6 @@ export class HaDataTable extends LitElement {
@property({ attribute: false }) public initialCollapsedGroups?: string[]; @property({ attribute: false }) public initialCollapsedGroups?: string[];
@property({ attribute: false }) public hiddenColumns?: string[];
@property({ attribute: false }) public columnOrder?: string[];
@state() private _filterable = false; @state() private _filterable = false;
@state() private _filter = ""; @state() private _filter = "";
@@ -249,7 +235,6 @@ export class HaDataTable extends LitElement {
(column: ClonedDataTableColumnData) => { (column: ClonedDataTableColumnData) => {
delete column.title; delete column.title;
delete column.template; delete column.template;
delete column.extraTemplate;
} }
); );
@@ -287,46 +272,12 @@ export class HaDataTable extends LitElement {
this._sortFilterData(); this._sortFilterData();
} }
if (properties.has("selectable") || properties.has("hiddenColumns")) { if (properties.has("selectable")) {
this._items = [...this._items]; this._items = [...this._items];
} }
} }
private _sortedColumns = memoizeOne(
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
if (!columnOrder || !columnOrder.length) {
return columns;
}
return Object.keys(columns)
.sort((a, b) => {
const orderA = columnOrder!.indexOf(a);
const orderB = columnOrder!.indexOf(b);
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce((obj, key) => {
obj[key] = columns[key];
return obj;
}, {}) as DataTableColumnContainer;
}
);
protected render() { protected render() {
const localize = this.localizeFunc || this.hass.localize;
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
this._renderRow(columns, this.narrow, row, index);
return html` return html`
<div class="mdc-data-table"> <div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcTableHeight}> <slot name="header" @slotchange=${this._calcTableHeight}>
@@ -375,15 +326,9 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(columns).map(([key, column]) => { ${Object.entries(this.columns).map(([key, column]) => {
if ( if (column.hidden) {
column.hidden || return "";
(this.columnOrder && this.columnOrder.includes(key)
? (this.hiddenColumns?.includes(key) ??
column.defaultHidden)
: column.defaultHidden)
) {
return nothing;
} }
const sorted = key === this.sortColumn; const sorted = key === this.sortColumn;
const classes = { const classes = {
@@ -442,7 +387,7 @@ export class HaDataTable extends LitElement {
<div class="mdc-data-table__row" role="row"> <div class="mdc-data-table__row" role="row">
<div class="mdc-data-table__cell grows center" role="cell"> <div class="mdc-data-table__cell grows center" role="cell">
${this.noDataText || ${this.noDataText ||
localize("ui.components.data-table.no-data")} this.hass.localize("ui.components.data-table.no-data")}
</div> </div>
</div> </div>
</div> </div>
@@ -454,7 +399,7 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos} @scroll=${this._saveScrollPos}
.items=${this._items} .items=${this._items}
.keyFunction=${this._keyFunction} .keyFunction=${this._keyFunction}
.renderItem=${renderRow} .renderItem=${this._renderRow}
></lit-virtualizer> ></lit-virtualizer>
`} `}
</div> </div>
@@ -464,12 +409,7 @@ export class HaDataTable extends LitElement {
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
private _renderRow = ( private _renderRow = (row: DataTableRowData, index: number) => {
columns: DataTableColumnContainer,
narrow: boolean,
row: DataTableRowData,
index: number
) => {
// not sure how this happens... // not sure how this happens...
if (!row) { if (!row) {
return nothing; return nothing;
@@ -514,14 +454,8 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(columns).map(([key, column]) => { ${Object.entries(this.columns).map(([key, column]) => {
if ( if (column.hidden) {
(narrow && !column.main && !column.showNarrow) ||
column.hidden ||
(this.columnOrder && this.columnOrder.includes(key)
? (this.hiddenColumns?.includes(key) ?? column.defaultHidden)
: column.defaultHidden)
) {
return nothing; return nothing;
} }
return html` return html`
@@ -548,38 +482,7 @@ export class HaDataTable extends LitElement {
}) })
: ""} : ""}
> >
${column.template ${column.template ? column.template(row) : row[key]}
? column.template(row)
: narrow && column.main
? html`<div class="primary">${row[key]}</div>
<div class="secondary">
${Object.entries(columns)
.filter(
([key2, column2]) =>
!column2.hidden &&
!column2.main &&
!column2.showNarrow &&
!(this.columnOrder &&
this.columnOrder.includes(key2)
? (this.hiddenColumns?.includes(key2) ??
column2.defaultHidden)
: column2.defaultHidden)
)
.map(
([key2, column2], i) =>
html`${i !== 0
? " ⸱ "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
)}
</div>
${column.extraTemplate
? column.extraTemplate(row)
: nothing}`
: html`${row[key]}${column.extraTemplate
? column.extraTemplate(row)
: nothing}`}
</div> </div>
`; `;
})} })}
@@ -625,8 +528,6 @@ export class HaDataTable extends LitElement {
return; return;
} }
const localize = this.localizeFunc || this.hass.localize;
if (this.appendRow || this.hasFab || this.groupColumn) { if (this.appendRow || this.hasFab || this.groupColumn) {
let items = [...data]; let items = [...data];
@@ -680,7 +581,7 @@ export class HaDataTable extends LitElement {
> >
</ha-icon-button> </ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY ${groupName === UNDEFINED_GROUP_KEY
? localize("ui.components.data-table.ungrouped") ? this.hass.localize("ui.components.data-table.ungrouped")
: groupName || ""} : groupName || ""}
</div>`, </div>`,
}); });
@@ -960,7 +861,6 @@ export class HaDataTable extends LitElement {
width: 100%; width: 100%;
border: 0; border: 0;
white-space: nowrap; white-space: nowrap;
position: relative;
} }
.mdc-data-table__cell { .mdc-data-table__cell {

View File

@@ -1,28 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc } from "../../common/translations/localize";
import { DataTableColumnContainer } from "./ha-data-table";
export interface DataTableSettingsDialogParams {
columns: DataTableColumnContainer;
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => void;
hiddenColumns?: string[];
columnOrder?: string[];
localizeFunc?: LocalizeFunc;
}
export const loadDataTableSettingsDialog = () =>
import("./dialog-data-table-settings");
export const showDataTableSettingsDialog = (
element: HTMLElement,
dialogParams: DataTableSettingsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-data-table-settings",
dialogImport: loadDataTableSettingsDialog,
dialogParams,
});
};

View File

@@ -1,6 +1,5 @@
import { expose } from "comlink"; import { expose } from "comlink";
import { stringCompare } from "../../common/string/compare"; import { stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import type { import type {
ClonedDataTableColumnData, ClonedDataTableColumnData,
DataTableRowData, DataTableRowData,
@@ -13,18 +12,20 @@ const filterData = (
columns: SortableColumnContainer, columns: SortableColumnContainer,
filter: string filter: string
) => { ) => {
filter = stripDiacritics(filter.toLowerCase()); filter = filter.toUpperCase();
return data.filter((row) => return data.filter((row) =>
Object.entries(columns).some((columnEntry) => { Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
const value = String( if (
column.filterKey String(
? row[column.valueColumn || key][column.filterKey] column.filterKey
: row[column.valueColumn || key] ? row[column.valueColumn || key][column.filterKey]
); : row[column.valueColumn || key]
)
if (stripDiacritics(value).toLowerCase().includes(filter)) { .toUpperCase()
.includes(filter)
) {
return true; return true;
} }
} }

View File

@@ -1,314 +0,0 @@
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;
}
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;
}
}

View File

@@ -134,7 +134,7 @@ export class HaStateLabelBadge extends LitElement {
this._timerTimeRemaining this._timerTimeRemaining
)} )}
.description=${this.showName .description=${this.showName
? (this.name ?? computeStateName(entityState)) ? this.name ?? computeStateName(entityState)
: undefined} : undefined}
> >
${!image && showIcon ${!image && showIcon

View File

@@ -90,8 +90,7 @@ class HaAnsiToHtml extends LitElement {
private _parseTextToColoredPre(text) { private _parseTextToColoredPre(text) {
const pre = document.createElement("pre"); const pre = document.createElement("pre");
// eslint-disable-next-line no-control-regex const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0; let i = 0;
const state: State = { const state: State = {

View File

@@ -94,8 +94,6 @@ export const computeInitialHaFormData = (
data[field.name] = selector.color_temp?.min_mireds ?? 153; data[field.name] = selector.color_temp?.min_mireds ?? 153;
} else if ( } else if (
"action" in selector || "action" in selector ||
"trigger" in selector ||
"condition" in selector ||
"media" in selector || "media" in selector ||
"target" in selector "target" in selector
) { ) {

View File

@@ -7,10 +7,9 @@ import { mdiRestore } from "@mdi/js";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = { type GridSizeValue = {
rows?: number | "auto"; rows?: number;
columns?: number; columns?: number;
}; };
@@ -20,7 +19,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public value?: GridSizeValue; @property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 8; @property({ attribute: false }) public rows = 6;
@property({ attribute: false }) public columns = 4; @property({ attribute: false }) public columns = 4;
@@ -43,20 +42,6 @@ export class HaGridSizeEditor extends LitElement {
} }
protected render() { protected render() {
const disabledColumns =
this.columnMin !== undefined && this.columnMin === this.columnMax;
const disabledRows =
this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto";
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
const columnMin = this.columnMin ?? 1;
const columnMax = this.columnMax ?? this.columns;
const rowValue = autoHeight ? rowMin : this._localValue?.rows;
const columnValue = this._localValue?.columns;
return html` return html`
<div class="grid"> <div class="grid">
<ha-grid-layout-slider <ha-grid-layout-slider
@@ -64,28 +49,25 @@ export class HaGridSizeEditor extends LitElement {
"ui.components.grid-size-picker.columns" "ui.components.grid-size-picker.columns"
)} )}
id="columns" id="columns"
.min=${columnMin} .min=${this.columnMin ?? 1}
.max=${columnMax} .max=${this.columnMax ?? this.columns}
.range=${this.columns} .range=${this.columns}
.value=${columnValue} .value=${this.value?.columns}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
></ha-grid-layout-slider> ></ha-grid-layout-slider>
<ha-grid-layout-slider <ha-grid-layout-slider
aria-label=${this.hass.localize( aria-label=${this.hass.localize(
"ui.components.grid-size-picker.rows" "ui.components.grid-size-picker.rows"
)} )}
id="rows" id="rows"
.min=${rowMin} .min=${this.rowMin ?? 1}
.max=${rowMax} .max=${this.rowMax ?? this.rows}
.range=${this.rows} .range=${this.rows}
vertical vertical
.value=${rowValue} .value=${this.value?.rows}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved} @slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
></ha-grid-layout-slider> ></ha-grid-layout-slider>
${!this.isDefault ${!this.isDefault
? html` ? html`
@@ -108,8 +90,8 @@ export class HaGridSizeEditor extends LitElement {
style=${styleMap({ style=${styleMap({
"--total-rows": this.rows, "--total-rows": this.rows,
"--total-columns": this.columns, "--total-columns": this.columns,
"--rows": rowValue, "--rows": this._localValue?.rows,
"--columns": columnValue, "--columns": this._localValue?.columns,
})} })}
> >
<div> <div>
@@ -118,11 +100,17 @@ export class HaGridSizeEditor extends LitElement {
.map((_, index) => { .map((_, index) => {
const row = Math.floor(index / this.columns) + 1; const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1; const column = (index % this.columns) + 1;
const disabled =
(this.rowMin !== undefined && row < this.rowMin) ||
(this.rowMax !== undefined && row > this.rowMax) ||
(this.columnMin !== undefined && column < this.columnMin) ||
(this.columnMax !== undefined && column > this.columnMax);
return html` return html`
<div <div
class="cell" class="cell"
data-row=${row} data-row=${row}
data-column=${column} data-column=${column}
?disabled=${disabled}
@click=${this._cellClick} @click=${this._cellClick}
></div> ></div>
`; `;
@@ -138,16 +126,11 @@ export class HaGridSizeEditor extends LitElement {
_cellClick(ev) { _cellClick(ev) {
const cell = ev.currentTarget as HTMLElement; const cell = ev.currentTarget as HTMLElement;
if (cell.getAttribute("disabled") !== null) return;
const rows = Number(cell.getAttribute("data-row")); const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column")); const columns = Number(cell.getAttribute("data-column"));
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
const clampedColumn = conditionalClamp(
columns,
this.columnMin,
this.columnMax
);
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn }, value: { rows, columns },
}); });
} }
@@ -205,7 +188,7 @@ export class HaGridSizeEditor extends LitElement {
.preview { .preview {
position: relative; position: relative;
grid-area: preview; grid-area: preview;
aspect-ratio: 1 / 1.2; aspect-ratio: 1 / 1;
} }
.preview > div { .preview > div {
position: absolute; position: absolute;
@@ -226,6 +209,10 @@ export class HaGridSizeEditor extends LitElement {
opacity: 0.2; opacity: 0.2;
cursor: pointer; cursor: pointer;
} }
.preview .cell[disabled] {
opacity: 0.05;
cursor: initial;
}
.selected { .selected {
pointer-events: none; pointer-events: none;
} }

View File

@@ -1,11 +1,9 @@
import { MdMenuItem } from "@material/web/menu/menu-item"; import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-menu-item") @customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem { export class HaMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`

View File

@@ -1,30 +1,9 @@
import { MdMenu } from "@material/web/menu/menu"; import { MdMenu } from "@material/web/menu/menu";
import type { CloseMenuEvent } from "@material/web/menu/menu";
import {
CloseReason,
KeydownCloseKey,
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { HaMenuItem } from "./ha-menu-item";
@customElement("ha-menu") @customElement("ha-menu")
export class HaMenu extends MdMenu { export class HaMenu extends MdMenu {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("close-menu", this._handleCloseMenu);
}
private _handleCloseMenu(ev: CloseMenuEvent) {
if (
ev.detail.reason.kind === CloseReason.KEYDOWN &&
ev.detail.reason.key === KeydownCloseKey.ESCAPE
) {
return;
}
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@@ -39,8 +18,4 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-menu": HaMenu; "ha-menu": HaMenu;
} }
interface HTMLElementEventMap {
"close-menu": CloseMenuEvent;
}
} }

View File

@@ -1,92 +1,72 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiCamera } from "@mdi/js"; import { mdiCamera } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner"; import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
import "./ha-button-menu"; import "./ha-button-menu";
import "./ha-list-item";
import "./ha-textfield"; import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
@customElement("ha-qr-scanner") @customElement("ha-qr-scanner")
class HaQrScanner extends LitElement { class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc; @property({ attribute: false }) public localize!: LocalizeFunc;
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
@property() public error?: string;
@state() private _cameras?: QrScanner.Camera[]; @state() private _cameras?: QrScanner.Camera[];
@state() private _manual = false; @state() private _error?: string;
private _qrScanner?: QrScanner; private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0; private _qrNotFoundCount = 0;
private _removeListener?: UnsubscribeFunc; @query("video", true) private _video!: HTMLVideoElement;
@query("video", true) private _video?: HTMLVideoElement; @query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
@query("ha-textfield") private _manualInput?: HaTextField; @query("ha-textfield") private _manualInput?: HaTextField;
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._qrNotFoundCount = 0; this._qrNotFoundCount = 0;
if (this._nativeBarcodeScanner) {
this._closeExternalScanner();
}
if (this._qrScanner) { if (this._qrScanner) {
this._qrScanner.stop(); this._qrScanner.stop();
this._qrScanner.destroy(); this._qrScanner.destroy();
this._qrScanner = undefined; this._qrScanner = undefined;
} }
while (this._canvasContainer?.lastChild) { while (this._canvasContainer.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild); this._canvasContainer.removeChild(this._canvasContainer.lastChild);
} }
} }
public connectedCallback(): void { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated) { if (this.hasUpdated && navigator.mediaDevices) {
this._loadQrScanner(); this._loadQrScanner();
} }
} }
protected firstUpdated() { protected firstUpdated() {
this._loadQrScanner(); if (navigator.mediaDevices) {
this._loadQrScanner();
}
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("error") && this.error) { if (changedProps.has("_error") && this._error) {
alert(`error: ${this.error}`); fireEvent(this, "qr-code-error", { message: this._error });
this._notifyExternalScanner(this.error);
} }
} }
protected render() { protected render(): TemplateResult {
if (this._nativeBarcodeScanner && !this._manual) { return html`${this._error
return nothing; ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
}
return html`${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""} : ""}
${navigator.mediaDevices && !this._manual ${navigator.mediaDevices
? html`<video></video> ? html`<video></video>
<div id="canvas-container"> <div id="canvas-container">
${this._cameras && this._cameras.length > 1 ${this._cameras && this._cameras.length > 1
@@ -100,26 +80,21 @@ class HaQrScanner extends LitElement {
></ha-icon-button> ></ha-icon-button>
${this._cameras!.map( ${this._cameras!.map(
(camera) => html` (camera) => html`
<ha-list-item <mwc-list-item
.value=${camera.id} .value=${camera.id}
@click=${this._cameraChanged} @click=${this._cameraChanged}
>${camera.label}</mwc-list-item
> >
${camera.label}
</ha-list-item>
` `
)} )}
</ha-button-menu>` </ha-button-menu>`
: nothing} : ""}
</div>` </div>`
: html`${this._manual : html`<ha-alert alert-type="warning">
? nothing ${!window.isSecureContext
: html`<ha-alert alert-type="warning"> ? this.localize("ui.components.qr-scanner.only_https_supported")
${!window.isSecureContext : this.localize("ui.components.qr-scanner.not_supported")}
? this.localize( </ha-alert>
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p> <p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row"> <div class="row">
<ha-textfield <ha-textfield
@@ -127,44 +102,33 @@ class HaQrScanner extends LitElement {
@keyup=${this._manualKeyup} @keyup=${this._manualKeyup}
@paste=${this._manualPaste} @paste=${this._manualPaste}
></ha-textfield> ></ha-textfield>
<mwc-button @click=${this._manualSubmit}> <mwc-button @click=${this._manualSubmit}
${this.localize("ui.common.submit")} >${this.localize("ui.common.submit")}</mwc-button
</mwc-button> >
</div>`}`; </div>`}`;
} }
private get _nativeBarcodeScanner(): boolean {
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
}
private async _loadQrScanner() { private async _loadQrScanner() {
if (this._nativeBarcodeScanner) {
this._openExternalScanner();
return;
}
if (!navigator.mediaDevices) {
return;
}
const QrScanner = (await import("qr-scanner")).default; const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) { if (!(await QrScanner.hasCamera())) {
this._reportError("No camera found"); this._error = "No camera found";
return; return;
} }
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js"; QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner); this._listCameras(QrScanner);
this._qrScanner = new QrScanner( this._qrScanner = new QrScanner(
this._video!, this._video,
this._qrCodeScanned, this._qrCodeScanned,
this._qrCodeError this._qrCodeError
); );
// @ts-ignore // @ts-ignore
const canvas = this._qrScanner.$canvas; const canvas = this._qrScanner.$canvas;
this._canvasContainer!.appendChild(canvas); this._canvasContainer.appendChild(canvas);
canvas.style.display = "block"; canvas.style.display = "block";
try { try {
await this._qrScanner.start(); await this._qrScanner.start();
} catch (err: any) { } catch (err: any) {
this._reportError(err); this._error = err;
} }
} }
@@ -176,16 +140,16 @@ class HaQrScanner extends LitElement {
if (err === "No QR code found") { if (err === "No QR code found") {
this._qrNotFoundCount++; this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) { if (this._qrNotFoundCount === 250) {
this._reportError(err); this._error = err;
} }
return; return;
} }
this._reportError(err.message || err); this._error = err.message || err;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(err); console.log(err);
}; };
private _qrCodeScanned = (qrCodeString: string): void => { private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
this._qrNotFoundCount = 0; this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString }); fireEvent(this, "qr-code-scanned", { value: qrCodeString });
}; };
@@ -211,62 +175,6 @@ class HaQrScanner extends LitElement {
this._qrScanner?.setCamera((ev.target as any).value); this._qrScanner?.setCamera((ev.target as any).value);
} }
private _openExternalScanner() {
this._removeListener = addExternalBarCodeListener((msg) => {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
}
} else if (msg.command === "bar_code/aborted") {
this._closeExternalScanner();
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
this._manual = true;
}
}
return true;
});
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title: this.title || "Scan QR code",
description: this.description || "Scan a barcode.",
alternative_option_label:
this.alternativeOptionLabel || "Click to manually enter the barcode",
},
});
}
private _closeExternalScanner() {
this._removeListener?.();
this._removeListener = undefined;
this.hass.auth.external!.fireMessage({
type: "bar_code/close",
});
}
private _notifyExternalScanner(message: string) {
if (!this.hass.auth.external) {
return;
}
this.hass.auth.external.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
this.error = undefined;
}
private _reportError(message: string) {
fireEvent(this, "qr-code-error", { message });
}
static styles = css` static styles = css`
canvas { canvas {
width: 100%; width: 100%;
@@ -302,7 +210,6 @@ declare global {
interface HASSDomEvents { interface HASSDomEvents {
"qr-code-scanned": { value: string }; "qr-code-scanned": { value: string };
"qr-code-error": { message: string }; "qr-code-error": { message: string };
"qr-code-closed": undefined;
} }
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {

View File

@@ -35,6 +35,10 @@ export class HaActionSelector extends LitElement {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label { label {
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;

View File

@@ -11,7 +11,6 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { AreaSelector } from "../../data/selector"; import type { AreaSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
@@ -38,8 +37,6 @@ export class HaAreaSelector extends LitElement {
@state() private _entitySources?: EntitySources; @state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: AreaSelector) { private _hasIntegration(selector: AreaSelector) {
@@ -75,12 +72,6 @@ export class HaAreaSelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
} }
protected render() { protected render() {
@@ -145,9 +136,7 @@ export class HaAreaSelector extends LitElement {
const deviceIntegrations = this._entitySources const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup( ? this._deviceIntegrationLookup(
this._entitySources, this._entitySources,
Object.values(this.hass.entities), Object.values(this.hass.entities)
Object.values(this.hass.devices),
this._configEntries
) )
: undefined; : undefined;

View File

@@ -35,6 +35,10 @@ export class HaConditionSelector extends LitElement {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
:host([disabled]) ha-automation-condition {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label { label {
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;

View File

@@ -11,7 +11,6 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { DeviceSelector } from "../../data/selector"; import type { DeviceSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
@@ -28,8 +27,6 @@ export class HaDeviceSelector extends LitElement {
@state() private _entitySources?: EntitySources; @state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
@property() public value?: any; @property() public value?: any;
@property() public label?: string; @property() public label?: string;
@@ -78,12 +75,6 @@ export class HaDeviceSelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
} }
protected render() { protected render() {
@@ -132,9 +123,7 @@ export class HaDeviceSelector extends LitElement {
const deviceIntegrations = this._entitySources const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup( ? this._deviceIntegrationLookup(
this._entitySources, this._entitySources,
Object.values(this.hass.entities), Object.values(this.hass.entities)
Object.values(this.hass.devices),
this._configEntries
) )
: undefined; : undefined;

View File

@@ -11,7 +11,6 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { FloorSelector } from "../../data/selector"; import type { FloorSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
@@ -38,8 +37,6 @@ export class HaFloorSelector extends LitElement {
@state() private _entitySources?: EntitySources; @state() private _entitySources?: EntitySources;
@state() private _configEntries?: ConfigEntry[];
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: FloorSelector) { private _hasIntegration(selector: FloorSelector) {
@@ -75,12 +72,6 @@ export class HaFloorSelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (!this._configEntries && this._hasIntegration(this.selector)) {
this._configEntries = [];
getConfigEntries(this.hass).then((entries) => {
this._configEntries = entries;
});
}
} }
protected render() { protected render() {
@@ -145,9 +136,7 @@ export class HaFloorSelector extends LitElement {
const deviceIntegrations = this._entitySources const deviceIntegrations = this._entitySources
? this._deviceIntegrationLookup( ? this._deviceIntegrationLookup(
this._entitySources, this._entitySources,
Object.values(this.hass.entities), Object.values(this.hass.entities)
Object.values(this.hass.devices),
this._configEntries
) )
: undefined; : undefined;

View File

@@ -35,6 +35,10 @@ export class HaTriggerSelector extends LitElement {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
:host([disabled]) ha-automation-trigger {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label { label {
display: block; display: block;
margin-bottom: 4px; margin-bottom: 4px;

View File

@@ -1,48 +0,0 @@
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;
}
}

View File

@@ -57,7 +57,6 @@ const LOAD_ELEMENTS = {
color_temp: () => import("./ha-selector-color-temp"), color_temp: () => import("./ha-selector-color-temp"),
ui_action: () => import("./ha-selector-ui-action"), ui_action: () => import("./ha-selector-ui-action"),
ui_color: () => import("./ha-selector-ui-color"), 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"]); const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);

View File

@@ -65,8 +65,6 @@ interface ExtHassService extends Omit<HassService, "fields"> {
Omit<HassService["fields"][string], "selector"> & { Omit<HassService["fields"][string], "selector"> & {
key: string; key: string;
selector?: Selector; selector?: Selector;
fields?: Record<string, Omit<HassService["fields"][string], "selector">>;
collapsed?: boolean;
} }
>; >;
hasSelector: string[]; hasSelector: string[];
@@ -249,7 +247,20 @@ export class HaServiceControl extends LitElement {
} }
); );
private _getTargetedEntities = memoizeOne((target, value) => { private _filterFields = memoizeOne(
(serviceData: ExtHassService | undefined, value: this["value"]) =>
serviceData?.fields?.filter(
(field) =>
!field.filter ||
this._filterField(serviceData.target, field.filter, value)
)
);
private _filterField(
target: ExtHassService["target"],
filter: ExtHassService["fields"][number]["filter"],
value: this["value"]
) {
const targetSelector = target ? { target } : { target: {} }; const targetSelector = target ? { target } : { target: {} };
const targetEntities = const targetEntities =
ensureArray( ensureArray(
@@ -319,13 +330,6 @@ export class HaServiceControl extends LitElement {
); );
}); });
} }
return targetEntities;
});
private _filterField(
filter: ExtHassService["fields"][number]["filter"],
targetEntities: string[]
) {
if (!targetEntities.length) { if (!targetEntities.length) {
return false; return false;
} }
@@ -387,10 +391,7 @@ export class HaServiceControl extends LitElement {
serviceData?.fields.some((field) => showOptionalToggle(field)) serviceData?.fields.some((field) => showOptionalToggle(field))
); );
const targetEntities = this._getTargetedEntities( const filteredFields = this._filterFields(serviceData, this._value);
serviceData?.target,
this._value
);
const domain = this._value?.service const domain = this._value?.service
? computeDomain(this._value.service) ? computeDomain(this._value.service)
@@ -451,7 +452,7 @@ export class HaServiceControl extends LitElement {
> >
<span slot="description" <span slot="description"
>${this.hass.localize( >${this.hass.localize(
"ui.components.service-control.target_secondary" "ui.components.service-control.target_description"
)}</span )}</span
><ha-selector ><ha-selector
.hass=${this.hass} .hass=${this.hass}
@@ -478,123 +479,81 @@ export class HaServiceControl extends LitElement {
${shouldRenderServiceDataYaml ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize( .label=${this.hass.localize("ui.components.service-control.data")}
"ui.components.service-control.action_data"
)}
.name=${"data"} .name=${"data"}
.readOnly=${this.disabled} .readOnly=${this.disabled}
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => : filteredFields?.map((dataField) => {
dataField.fields const selector = dataField?.selector ?? { text: undefined };
? html`<ha-expansion-panel const type = Object.keys(selector)[0];
leftChevron const enhancedSelector = ["action", "condition", "trigger"].includes(
.expanded=${!dataField.collapsed} type
.header=${this.hass.localize( )
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name` ? {
) || [type]: {
dataField.name || ...selector[type],
dataField.key} path: [dataField.key],
> },
${Object.entries(dataField.fields).map(([key, field]) => }
this._renderField( : selector;
{ key, ...field },
hasOptional, const showOptional = showOptionalToggle(dataField);
domain,
serviceName, return dataField.selector &&
targetEntities (!dataField.advanced ||
) this.showAdvanced ||
)} (this._value?.data &&
</ha-expansion-panel>` this._value.data[dataField.key] !== undefined))
: this._renderField( ? html`<ha-settings-row .narrow=${this.narrow}>
dataField, ${!showOptional
hasOptional, ? hasOptional
domain, ? html`<div slot="prefix" class="checkbox-spacer"></div>`
serviceName, : ""
targetEntities : html`<ha-checkbox
) .key=${dataField.key}
)} `; .checked=${this._checkedKeys.has(dataField.key) ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined)}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data
? this._value.data[dataField.key]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
</ha-settings-row>`
: "";
})} `;
} }
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,
domain: string | undefined,
serviceName: string | undefined,
targetEntities: string[]
) => {
if (
dataField.filter &&
!this._filterField(dataField.filter, targetEntities)
) {
return nothing;
}
const selector = dataField?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
? {
[type]: {
...selector[type],
path: [dataField.key],
},
}
: selector;
const showOptional = showOptionalToggle(dataField);
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data && this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
: html`<ha-checkbox
.key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined)}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(dataField.key) &&
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data
? this._value.data[dataField.key]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
</ha-settings-row>`
: "";
};
private _localizeValueCallback = (key: string) => { private _localizeValueCallback = (key: string) => {
if (!this._value?.service) { if (!this._value?.service) {
return ""; return "";
@@ -880,11 +839,6 @@ export class HaServiceControl extends LitElement {
.description p { .description p {
direction: ltr; direction: ltr;
} }
ha-expansion-panel {
--ha-card-border-radius: 0;
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0;
}
`; `;
} }
} }

View File

@@ -46,7 +46,7 @@ class HaServicePicker extends LitElement {
return html` return html`
<ha-combo-box <ha-combo-box
.hass=${this.hass} .hass=${this.hass}
.label=${this.hass.localize("ui.components.service-picker.action")} .label=${this.hass.localize("ui.components.service-picker.service")}
.filteredItems=${this._filteredServices( .filteredItems=${this._filteredServices(
this.hass.localize, this.hass.localize,
this.hass.services, this.hass.services,

View File

@@ -210,8 +210,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _editStyleLoaded = false; private _editStyleLoaded = false;
private _unsubPersistentNotifications: UnsubscribeFunc | undefined;
@storage({ @storage({
key: "sidebarPanelOrder", key: "sidebarPanelOrder",
state: true, state: true,
@@ -285,26 +283,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.locale !== oldHass.locale || hass.locale !== oldHass.locale ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel || hass.defaultPanel !== oldHass.defaultPanel
hass.connected !== oldHass.connected
); );
} }
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.subscribePersistentNotifications(); subscribeNotifications(this.hass.connection, (notifications) => {
} this._notifications = notifications;
});
private subscribePersistentNotifications(): void {
if (this._unsubPersistentNotifications) {
this._unsubPersistentNotifications();
}
this._unsubPersistentNotifications = subscribeNotifications(
this.hass.connection,
(notifications) => {
this._notifications = notifications;
}
);
} }
protected updated(changedProps) { protected updated(changedProps) {
@@ -319,14 +306,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return; return;
} }
if (
this.hass &&
changedProps.get("hass")?.connected === false &&
this.hass.connected === true
) {
this.subscribePersistentNotifications();
}
this._calculateCounts(); this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) { if (!SUPPORT_SCROLL_IF_NEEDED) {

View File

@@ -15,7 +15,6 @@ export class HaSlider extends MdSlider {
css` css`
:host { :host {
--md-sys-color-primary: var(--primary-color); --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-outline: var(--outline-color);
--md-sys-color-on-surface: var(--primary-text-color); --md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px; --md-slider-handle-width: 14px;

View File

@@ -483,12 +483,12 @@ export class HaMap extends ReactiveElement {
const entityName = const entityName =
typeof entity !== "string" && entity.label_mode === "state" typeof entity !== "string" && entity.label_mode === "state"
? this.hass.formatEntityState(stateObj) ? this.hass.formatEntityState(stateObj)
: (customTitle ?? : customTitle ??
title title
.split(" ") .split(" ")
.map((part) => part[0]) .map((part) => part[0])
.join("") .join("")
.substr(0, 3)); .substr(0, 3);
// create marker with the icon // create marker with the icon
const marker = Leaflet.marker([latitude, longitude], { const marker = Leaflet.marker([latitude, longitude], {

View File

@@ -17,7 +17,7 @@ export class HaTileIcon extends LitElement {
return css` return css`
:host { :host {
--tile-icon-color: var(--disabled-color); --tile-icon-color: var(--disabled-color);
--mdc-icon-size: 22px; --mdc-icon-size: 24px;
} }
.shape::before { .shape::before {
content: ""; content: "";
@@ -32,9 +32,9 @@ export class HaTileIcon extends LitElement {
} }
.shape { .shape {
position: relative; position: relative;
width: 36px; width: 40px;
height: 36px; height: 40px;
border-radius: 18px; border-radius: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -25,9 +25,9 @@ export class HaTileImage extends LitElement {
return css` return css`
.image { .image {
position: relative; position: relative;
width: 36px; width: 40px;
height: 36px; height: 40px;
border-radius: 18px; border-radius: 20px;
display: flex; display: flex;
flex: none; flex: none;
align-items: center; align-items: center;

View File

@@ -33,7 +33,7 @@ export class HaTileInfo extends LitElement {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
height: 36px; min-height: 40px;
} }
span { span {
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -352,22 +352,6 @@ export const saveAutomationConfig = (
config: AutomationConfig config: AutomationConfig
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config); ) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
export const normalizeAutomationConfig = <
T extends Partial<AutomationConfig> | AutomationConfig,
>(
config: T
): T => {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
return config;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => { export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
initialAutomationEditorData = data; initialAutomationEditorData = data;
navigate("/config/automation/edit/new"); navigate("/config/automation/edit/new");

View File

@@ -1,6 +1,4 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { ManualAutomationConfig } from "./automation";
import { ManualScriptConfig } from "./script";
import { Selector } from "./selector"; import { Selector } from "./selector";
export type BlueprintDomain = "automation" | "script"; export type BlueprintDomain = "automation" | "script";
@@ -44,11 +42,6 @@ export interface BlueprintImportResult {
validation_errors: string[] | null; validation_errors: string[] | null;
} }
export interface BlueprintSubstituteResults {
automation: { substituted_config: ManualAutomationConfig };
script: { substituted_config: ManualScriptConfig };
}
export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) => export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) =>
hass.callWS<Blueprints>({ type: "blueprint/list", domain }); hass.callWS<Blueprints>({ type: "blueprint/list", domain });
@@ -98,18 +91,3 @@ export const getBlueprintSourceType = (
} }
return "community"; return "community";
}; };
export const substituteBlueprint = <
T extends BlueprintDomain = BlueprintDomain,
>(
hass: HomeAssistant,
domain: T,
path: string,
input: Record<string, any>
) =>
hass.callWS<BlueprintSubstituteResults[T]>({
type: "blueprint/substitute",
domain,
path,
input,
});

View File

@@ -28,14 +28,13 @@ export type HvacMode = (typeof HVAC_MODES)[number];
export const CLIMATE_PRESET_NONE = "none"; export const CLIMATE_PRESET_NONE = "none";
export type HvacAction = export type HvacAction =
| "cooling"
| "defrosting"
| "drying"
| "fan"
| "heating"
| "idle"
| "off" | "off"
| "preheating"; | "preheating"
| "heating"
| "cooling"
| "drying"
| "idle"
| "fan";
export type ClimateEntity = HassEntityBase & { export type ClimateEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & { attributes: HassEntityAttributeBase & {
@@ -90,13 +89,12 @@ export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = { export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
cooling: "cool", cooling: "cool",
defrosting: "heat",
drying: "dry", drying: "dry",
fan: "fan_only", fan: "fan_only",
preheating: "heat",
heating: "heat", heating: "heat",
idle: "off", idle: "off",
off: "off", off: "off",
preheating: "heat",
}; };
export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = { export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {

View File

@@ -115,8 +115,8 @@ export function computeCoverPositionStateDisplay(
position?: number position?: number
) { ) {
const statePosition = stateActive(stateObj) const statePosition = stateActive(stateObj)
? (stateObj.attributes.current_position ?? ? stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position) stateObj.attributes.current_tilt_position
: undefined; : undefined;
const currentPosition = position ?? statePosition; const currentPosition = position ?? statePosition;

View File

@@ -5,7 +5,6 @@ import type {
EntityRegistryDisplayEntry, EntityRegistryDisplayEntry,
EntityRegistryEntry, EntityRegistryEntry,
} from "./entity_registry"; } from "./entity_registry";
import { ConfigEntry } from "./config_entries";
import type { EntitySources } from "./entity_sources"; import type { EntitySources } from "./entity_sources";
export { export {
@@ -20,7 +19,6 @@ export interface DeviceRegistryEntry {
identifiers: Array<[string, string]>; identifiers: Array<[string, string]>;
manufacturer: string | null; manufacturer: string | null;
model: string | null; model: string | null;
model_id: string | null;
name: string | null; name: string | null;
labels: string[]; labels: string[];
sw_version: string | null; sw_version: string | null;
@@ -144,11 +142,9 @@ export const getDeviceEntityDisplayLookup = (
export const getDeviceIntegrationLookup = ( export const getDeviceIntegrationLookup = (
entitySources: EntitySources, entitySources: EntitySources,
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[], entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
devices?: DeviceRegistryEntry[], ): Record<string, string[]> => {
configEntries?: ConfigEntry[] const deviceIntegrations: Record<string, string[]> = {};
): Record<string, Set<string>> => {
const deviceIntegrations: Record<string, Set<string>> = {};
for (const entity of entities) { for (const entity of entities) {
const source = entitySources[entity.entity_id]; const source = entitySources[entity.entity_id];
@@ -156,22 +152,10 @@ export const getDeviceIntegrationLookup = (
continue; continue;
} }
deviceIntegrations[entity.device_id!] = if (!deviceIntegrations[entity.device_id!]) {
deviceIntegrations[entity.device_id!] || new Set<string>(); deviceIntegrations[entity.device_id!] = [];
deviceIntegrations[entity.device_id!].add(source.domain);
}
// Lookup devices that have no entities
if (devices && configEntries) {
for (const device of devices) {
for (const config_entry_id of device.config_entries) {
const entry = configEntries.find((e) => e.entry_id === config_entry_id);
if (entry?.domain) {
deviceIntegrations[device.id] =
deviceIntegrations[device.id] || new Set<string>();
deviceIntegrations[device.id].add(entry.domain);
}
}
} }
deviceIntegrations[entity.device_id!].push(source.domain);
} }
return deviceIntegrations; return deviceIntegrations;
}; };

View File

@@ -17,8 +17,6 @@ export const enum FanEntityFeature {
OSCILLATE = 2, OSCILLATE = 2,
DIRECTION = 4, DIRECTION = 4,
PRESET_MODE = 8, PRESET_MODE = 8,
TURN_OFF = 16,
TURN_ON = 32,
} }
interface FanEntityAttributes extends HassEntityAttributeBase { interface FanEntityAttributes extends HassEntityAttributeBase {

View File

@@ -1,8 +1,10 @@
import { import {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
UnsubscribeFunc,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { HomeAssistant } from "../types";
interface GroupEntityAttributes extends HassEntityAttributeBase { interface GroupEntityAttributes extends HassEntityAttributeBase {
entity_id: string[]; entity_id: string[];
@@ -15,6 +17,11 @@ export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes; attributes: GroupEntityAttributes;
} }
export interface GroupPreview {
state: string;
attributes: Record<string, any>;
}
export const computeGroupDomain = ( export const computeGroupDomain = (
stateObj: GroupEntity stateObj: GroupEntity
): string | undefined => { ): string | undefined => {
@@ -24,3 +31,17 @@ export const computeGroupDomain = (
]; ];
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined; 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,
});

View File

@@ -3,10 +3,9 @@ import {
getCollection, getCollection,
HassEventBase, HassEventBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { HuiBadge } from "../panels/lovelace/badges/hui-badge";
import type { HuiCard } from "../panels/lovelace/cards/hui-card"; import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section"; import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import { Lovelace } from "../panels/lovelace/types"; import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section"; import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types"; import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
@@ -22,7 +21,7 @@ export interface LovelaceViewElement extends HTMLElement {
narrow?: boolean; narrow?: boolean;
index?: number; index?: number;
cards?: HuiCard[]; cards?: HuiCard[];
badges?: HuiBadge[]; badges?: LovelaceBadge[];
sections?: HuiSection[]; sections?: HuiSection[];
isStrategy: boolean; isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void; setConfig(config: LovelaceViewConfig): void;

View File

@@ -1,12 +1,4 @@
import { Condition } from "../../../panels/lovelace/common/validate-condition";
export interface LovelaceBadgeConfig { export interface LovelaceBadgeConfig {
type?: string; type?: string;
[key: string]: any; [key: string]: any;
visibility?: Condition[];
} }
export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({
type: "entity",
entity: entity_id,
});

View File

@@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig {
export interface LovelaceViewConfig extends LovelaceBaseViewConfig { export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string; type?: string;
badges?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id badges?: Array<string | LovelaceBadgeConfig>;
cards?: LovelaceCardConfig[]; cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[]; sections?: LovelaceSectionRawConfig[];
} }

View File

@@ -8,14 +8,6 @@ export interface CustomCardEntry {
documentationURL?: string; documentationURL?: string;
} }
export interface CustomBadgeEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
documentationURL?: string;
}
export interface CustomCardFeatureEntry { export interface CustomCardFeatureEntry {
type: string; type: string;
name?: string; name?: string;
@@ -26,7 +18,6 @@ export interface CustomCardFeatureEntry {
export interface CustomCardsWindow { export interface CustomCardsWindow {
customCards?: CustomCardEntry[]; customCards?: CustomCardEntry[];
customCardFeatures?: CustomCardFeatureEntry[]; customCardFeatures?: CustomCardFeatureEntry[];
customBadges?: CustomBadgeEntry[];
/** /**
* @deprecated Use customCardFeatures * @deprecated Use customCardFeatures
*/ */
@@ -43,9 +34,6 @@ if (!("customCards" in customCardsWindow)) {
if (!("customCardFeatures" in customCardsWindow)) { if (!("customCardFeatures" in customCardsWindow)) {
customCardsWindow.customCardFeatures = []; customCardsWindow.customCardFeatures = [];
} }
if (!("customBadges" in customCardsWindow)) {
customCardsWindow.customBadges = [];
}
if (!("customTileFeatures" in customCardsWindow)) { if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = []; customCardsWindow.customTileFeatures = [];
} }
@@ -55,14 +43,10 @@ export const getCustomCardFeatures = () => [
...customCardsWindow.customCardFeatures!, ...customCardsWindow.customCardFeatures!,
...customCardsWindow.customTileFeatures!, ...customCardsWindow.customTileFeatures!,
]; ];
export const customBadges = customCardsWindow.customBadges!;
export const getCustomCardEntry = (type: string) => export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type); customCards.find((card) => card.type === type);
export const getCustomBadgeEntry = (type: string) =>
customBadges.find((badge) => badge.type === type);
export const isCustomType = (type: string) => export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX); type.startsWith(CUSTOM_TYPE_PREFIX);

View File

@@ -5,44 +5,33 @@ export interface OTBRInfo {
border_agent_id: string; border_agent_id: string;
channel: number; channel: number;
extended_address: string; extended_address: string;
extended_pan_id: string;
url: string; url: string;
} }
export type OTBRInfoDict = Record<string, OTBRInfo>; export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfo> =>
export const getOTBRInfo = (hass: HomeAssistant): Promise<OTBRInfoDict> =>
hass.callWS({ hass.callWS({
type: "otbr/info", type: "otbr/info",
}); });
export const OTBRCreateNetwork = ( export const OTBRCreateNetwork = (hass: HomeAssistant): Promise<void> =>
hass: HomeAssistant,
extended_address: string
): Promise<void> =>
hass.callWS({ hass.callWS({
type: "otbr/create_network", type: "otbr/create_network",
extended_address,
}); });
export const OTBRSetNetwork = ( export const OTBRSetNetwork = (
hass: HomeAssistant, hass: HomeAssistant,
extended_address: string,
dataset_id: string dataset_id: string
): Promise<void> => ): Promise<void> =>
hass.callWS({ hass.callWS({
type: "otbr/set_network", type: "otbr/set_network",
extended_address,
dataset_id, dataset_id,
}); });
export const OTBRSetChannel = ( export const OTBRSetChannel = (
hass: HomeAssistant, hass: HomeAssistant,
extended_address: string,
channel: number channel: number
): Promise<{ delay: number }> => ): Promise<{ delay: number }> =>
hass.callWS({ hass.callWS({
type: "otbr/set_channel", type: "otbr/set_channel",
extended_address,
channel, channel,
}); });

View File

@@ -2,8 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature"; 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 { UiAction } from "../panels/lovelace/components/hui-action-editor";
import { HomeAssistant, ItemPath } from "../types"; import { HomeAssistant, ItemPath } from "../types";
import { import {
@@ -15,6 +13,8 @@ import {
EntityRegistryEntry, EntityRegistryEntry,
} from "./entity_registry"; } from "./entity_registry";
import { EntitySources } from "./entity_sources"; 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 = export type Selector =
| ActionSelector | ActionSelector
@@ -64,8 +64,7 @@ export type Selector =
| TTSSelector | TTSSelector
| TTSVoiceSelector | TTSVoiceSelector
| UiActionSelector | UiActionSelector
| UiColorSelector | UiColorSelector;
| UiStateContentSelector;
export interface ActionSelector { export interface ActionSelector {
action: { action: {
@@ -456,13 +455,6 @@ export interface UiColorSelector {
ui_color: { default_color?: boolean } | null; 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 = ( export const expandLabelTarget = (
hass: HomeAssistant, hass: HomeAssistant,
labelId: string, labelId: string,
@@ -704,7 +696,7 @@ export const entityMeetsTargetSelector = (
export const filterSelectorDevices = ( export const filterSelectorDevices = (
filterDevice: DeviceSelectorFilter, filterDevice: DeviceSelectorFilter,
device: DeviceRegistryEntry, device: DeviceRegistryEntry,
deviceIntegrationLookup?: Record<string, Set<string>> | undefined deviceIntegrationLookup?: Record<string, string[]> | undefined
): boolean => { ): boolean => {
const { const {
manufacturer: filterManufacturer, manufacturer: filterManufacturer,
@@ -721,7 +713,7 @@ export const filterSelectorDevices = (
} }
if (filterIntegration && deviceIntegrationLookup) { if (filterIntegration && deviceIntegrationLookup) {
if (!deviceIntegrationLookup?.[device.id]?.has(filterIntegration)) { if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) {
return false; return false;
} }
} }

View File

@@ -1,27 +1,21 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
const HAS_CUSTOM_PREVIEW = ["template"]; export interface ThresholdPreview {
export interface GenericPreview {
state: string; state: string;
attributes: Record<string, any>; attributes: Record<string, any>;
} }
export const subscribePreviewGeneric = ( export const subscribePreviewThreshold = (
hass: HomeAssistant, hass: HomeAssistant,
domain: string,
flow_id: string, flow_id: string,
flow_type: "config_flow" | "options_flow", flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>, user_input: Record<string, any>,
callback: (preview: GenericPreview) => void callback: (preview: ThresholdPreview) => void
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, { hass.connection.subscribeMessage(callback, {
type: `${domain}/start_preview`, type: "threshold/start_preview",
flow_id, flow_id,
flow_type, flow_type,
user_input, user_input,
}); });
export const previewModule = (domain: string): string =>
HAS_CUSTOM_PREVIEW.includes(domain) ? domain : "generic";

21
src/data/time_date.ts Normal file
View File

@@ -0,0 +1,21 @@
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,
});

View File

@@ -92,7 +92,7 @@ export const computeDisplayTimer = (
return hass.formatEntityState(stateObj); return hass.formatEntityState(stateObj);
} }
let display = secondsToDuration(timeRemaining || 0) || "0"; let display = secondsToDuration(timeRemaining || 0);
if (stateObj.state === "paused") { if (stateObj.state === "paused") {
display = `${display} (${hass.formatEntityState(stateObj)})`; display = `${display} (${hass.formatEntityState(stateObj)})`;

View File

@@ -0,0 +1,90 @@
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;
}
}

View File

@@ -2,22 +2,23 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow"; import { FlowType } from "../../../data/data_entry_flow";
import { GenericPreview, subscribePreviewGeneric } from "../../../data/preview"; import {
ThresholdPreview,
subscribePreviewThreshold,
} from "../../../data/threshold";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "./entity-preview-row"; import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce"; import { debounce } from "../../../common/util/debounce";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-generic") @customElement("flow-preview-threshold")
class FlowPreviewGeneric extends LitElement { class FlowPreviewThreshold extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType; @property() public flowType!: FlowType;
public handler!: string; public handler!: string;
@property() public domain!: string;
@property() public stepId!: string; @property() public stepId!: string;
@property() public flowId!: string; @property() public flowId!: string;
@@ -54,7 +55,7 @@ class FlowPreviewGeneric extends LitElement {
></entity-preview-row>`; ></entity-preview-row>`;
} }
private _setPreview = (preview: GenericPreview) => { private _setPreview = (preview: ThresholdPreview) => {
const now = new Date().toISOString(); const now = new Date().toISOString();
this._preview = { this._preview = {
entity_id: `${this.stepId}.___flow_preview___`, entity_id: `${this.stepId}.___flow_preview___`,
@@ -78,14 +79,14 @@ class FlowPreviewGeneric extends LitElement {
return; return;
} }
try { try {
this._unsub = subscribePreviewGeneric( this._unsub = subscribePreviewThreshold(
this.hass, this.hass,
this.domain,
this.flowId, this.flowId,
this.flowType, this.flowType,
this.stepData, this.stepData,
this._setPreview this._setPreview
); );
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} }); fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) { } catch (err: any) {
if (typeof err.message === "string") { if (typeof err.message === "string") {
@@ -102,6 +103,6 @@ class FlowPreviewGeneric extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"flow-preview-generic": FlowPreviewGeneric; "flow-preview-threshold": FlowPreviewThreshold;
} }
} }

View File

@@ -0,0 +1,94 @@
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;
}
}

View File

@@ -25,7 +25,6 @@ import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow"; import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import { previewModule } from "../../data/preview";
@customElement("step-flow-form") @customElement("step-flow-form")
class StepFlowForm extends LitElement { class StepFlowForm extends LitElement {
@@ -77,9 +76,8 @@ class StepFlowForm extends LitElement {
"ui.panel.config.integrations.config_flow.preview" "ui.panel.config.integrations.config_flow.preview"
)}: )}:
</h3> </h3>
${dynamicElement(`flow-preview-${previewModule(step.preview)}`, { ${dynamicElement(`flow-preview-${this.step.preview}`, {
hass: this.hass, hass: this.hass,
domain: step.preview,
flowType: this.flowConfig.flowType, flowType: this.flowConfig.flowType,
handler: step.handler, handler: step.handler,
stepId: step.step_id, stepId: step.step_id,
@@ -122,7 +120,7 @@ class StepFlowForm extends LitElement {
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (changedProps.has("step") && this.step?.preview) { if (changedProps.has("step") && this.step?.preview) {
import(`./previews/flow-preview-${previewModule(this.step.preview)}`); import(`./previews/flow-preview-${this.step.preview}`);
} }
} }

View File

@@ -325,7 +325,7 @@ export class MoreInfoDialog extends LitElement {
></ha-icon-button> ></ha-icon-button>
` `
: nothing} : nothing}
${!__DEMO__ && isAdmin ${isAdmin
? html` ? html`
<ha-icon-button <ha-icon-button
slot="actionItems" slot="actionItems"

View File

@@ -75,13 +75,11 @@ export class MoreInfoHistory extends LitElement {
<div class="title"> <div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")} ${this.hass.localize("ui.dialogs.more_info_control.history")}
</div> </div>
${__DEMO__ <a href=${this._showMoreHref} @click=${this._close}
? nothing >${this.hass.localize(
: html`<a href=${this._showMoreHref} @click=${this._close} "ui.dialogs.more_info_control.show_more"
>${this.hass.localize( )}</a
"ui.dialogs.more_info_control.show_more" >
)}</a
>`}
</div> </div>
${this._error ${this._error
? html`<div class="errors">${this._error}</div>` ? html`<div class="errors">${this._error}</div>`

View File

@@ -1,6 +1,7 @@
// Compat needs to be first import // Compat needs to be first import
import "../resources/compatibility"; import "../resources/compatibility";
import "../auth/ha-authorize"; import "../auth/ha-authorize";
import "../resources/safari-14-attachshadow-patch";
import("../resources/ha-style"); import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then( import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -25,6 +25,7 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user"; import { subscribeUser } from "../data/ws-user";
import type { ExternalAuth } from "../external_app/external_auth"; import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/safari-14-attachshadow-patch";
window.name = MAIN_WINDOW_NAME; window.name = MAIN_WINDOW_NAME;
(window as any).frontendVersion = __VERSION__; (window as any).frontendVersion = __VERSION__;

View File

@@ -1,5 +1,6 @@
// Compat needs to be first import // Compat needs to be first import
import "../resources/compatibility"; import "../resources/compatibility";
import "../resources/safari-14-attachshadow-patch";
import { CSSResult } from "lit"; import { CSSResult } from "lit";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -72,7 +73,7 @@ function initialize(
); );
} }
if (__BUILD__ === "legacy") { if (__BUILD__ === "es5") {
start = start.then(() => window.loadES5Adapter()); start = start.then(() => window.loadES5Adapter());
} }

View File

@@ -1,6 +1,7 @@
// Compat needs to be first import // Compat needs to be first import
import "../resources/compatibility"; import "../resources/compatibility";
import "../onboarding/ha-onboarding"; import "../onboarding/ha-onboarding";
import "../resources/safari-14-attachshadow-patch";
import("../resources/ha-style"); import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then( import("@polymer/polymer/lib/utils/settings").then(

View File

@@ -13,16 +13,18 @@ import {
StaleWhileRevalidate, StaleWhileRevalidate,
} from "workbox-strategies"; } from "workbox-strategies";
declare const __WB_MANIFEST__: Parameters<typeof precacheAndRoute>[0];
const noFallBackRegEx = const noFallBackRegEx =
/\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/; /\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/;
const initRouting = () => { const initRouting = () => {
precacheAndRoute(__WB_MANIFEST__, { precacheAndRoute(
// Ignore all URL parameters. // @ts-ignore
ignoreURLParametersMatching: [/.*/], WB_MANIFEST,
}); {
// Ignore all URL parameters.
ignoreURLParametersMatching: [/.*/],
}
);
// Cache static content (including translations) on first access. // Cache static content (including translations) on first access.
registerRoute( registerRoute(
@@ -54,8 +56,11 @@ const initRouting = () => {
// Get api from network. // Get api from network.
registerRoute(/\/(api|auth)\/.*/, new NetworkOnly()); registerRoute(/\/(api|auth)\/.*/, new NetworkOnly());
// Get manifest and onboarding from network. // Get manifest, service worker, onboarding from network.
registerRoute(/\/(?:manifest\.json|onboarding\.html)/, new NetworkOnly()); registerRoute(
/\/(service_worker.js|manifest.json|onboarding.html)/,
new NetworkOnly()
);
// For the root "/" we ignore search // For the root "/" we ignore search
registerRoute( registerRoute(

View File

@@ -128,9 +128,6 @@ interface EMOutgoingMessageAssistShow extends EMMessage {
start_listening: boolean; start_listening: boolean;
}; };
} }
interface EMOutgoingMessageImprovScan extends EMMessage {
type: "improv/scan";
}
interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage { interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
type: "thread/store_in_platform_keychain"; type: "thread/store_in_platform_keychain";
@@ -159,8 +156,7 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageSidebarShow | EMOutgoingMessageSidebarShow
| EMOutgoingMessageTagWrite | EMOutgoingMessageTagWrite
| EMOutgoingMessageThemeUpdate | EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain | EMOutgoingMessageThreadStoreInPlatformKeychain;
| EMOutgoingMessageImprovScan;
interface EMIncomingMessageRestart { interface EMIncomingMessageRestart {
id: number; id: number;
@@ -256,7 +252,6 @@ export interface ExternalConfig {
canTransferThreadCredentialsToKeychain: boolean; canTransferThreadCredentialsToKeychain: boolean;
hasAssist: boolean; hasAssist: boolean;
hasBarCodeScanner: number; hasBarCodeScanner: number;
canSetupImprov: boolean;
} }
export class ExternalMessaging { export class ExternalMessaging {

Some files were not shown because too many files have changed in this diff Show More