Compare commits

...

29 Commits

Author SHA1 Message Date
Bram Kragten
a548d13931 Allow partial open of sidebar 2023-04-01 18:46:52 +02:00
Bram Kragten
ddfe02eb70 Bumped version to 20230401.0 2023-04-01 18:20:48 +02:00
Bram Kragten
acaaf25500 Use ha-drawer instead of mwc-drawer (#16013) 2023-04-01 16:18:52 +00:00
renovate[bot]
c6c3e63101 Update dependency @web/dev-server to v0.1.37 (#16008) 2023-04-01 10:31:47 -04:00
renovate[bot]
e0fe4631f9 Update dependency eslint to v8.37.0 (#16007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-01 01:56:46 -04:00
Paulus Schoutsen
232f70d44c Allow having conversations (#15961)
* Allow having conversations

* Add timeout to run-start data

* willUpdate
2023-03-31 15:44:32 -04:00
Bram Kragten
273904a6eb Add support for supported_features for entity selector (#16003) 2023-03-31 15:07:41 -04:00
Steve Repsher
91caffc4e1 Improve bundle chunk hashes and names (#15991) 2023-03-31 12:49:25 -04:00
Bram Kragten
abcb904def Bumped version to 20230331.0 2023-03-31 16:37:33 +02:00
Bram Kragten
36c5d70597 Add sidebar actions to external bus (#15999) 2023-03-31 16:36:11 +02:00
Paul Bottein
b0b7998757 Fix for long translation in alarm more info (#16000) 2023-03-31 14:24:03 +00:00
Paul Bottein
33ec1e15a9 Fix labels on new more info (#15983) 2023-03-31 15:57:35 +02:00
Paul Bottein
d97ddcd31a Catch alarm control panel errors (#15998) 2023-03-31 15:51:44 +02:00
Paul Bottein
73c286a493 Fix ha header bar height (#15996)
* Fix ha header bar height in more info

* Fix ha header bar height in more places
2023-03-31 15:44:22 +02:00
Franck Nijhof
3e954eef02 Extend get_states (#15985) 2023-03-31 15:29:24 +02:00
Paul Bottein
a94b211d3e Fix ha-settings-row overflow (#15993) 2023-03-31 13:02:36 +02:00
renovate[bot]
1293e5f61f Update typescript-eslint monorepo to v5.57.0 (#15986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-30 15:35:02 -04:00
Bram Kragten
287b0b9235 Bumped version to 20230330.0 2023-03-30 16:58:32 +02:00
Bram Kragten
3d6743ae3e Align header paddings (#15981) 2023-03-30 16:57:25 +02:00
Bram Kragten
5193f2c6a4 Fix header when sidebar always hidden (#15980) 2023-03-30 16:46:31 +02:00
Paul Bottein
e6772e8b89 Fix fan more info state display (#15979) 2023-03-30 16:20:30 +02:00
Bram Kragten
dcac853b71 Improve graph tooltips (#15887
* Improve graph tooltips

* Use xy for state charts

* intersect, bigger hitRadius

* improve energy

* fix position tooltips
2023-03-30 16:12:26 +02:00
Bram Kragten
0df096d68b Fix overflow, change position of action-handler (#15978) 2023-03-30 16:10:54 +02:00
Bram Kragten
ef10cc77f7 Fix lovelace background, tweak headers (#15977) 2023-03-30 16:10:43 +02:00
Paul Bottein
e52b2c49a6 Fix mouse event for remove icon in ha file upload (#15976) 2023-03-30 11:59:50 +00:00
Paul Bottein
78cc75c57c Fix cover more info state display (#15974) 2023-03-30 09:55:13 +00:00
Bram Kragten
2b38a1ce33 Fix dialogs under sidebar, headers (#15973) 2023-03-30 11:46:34 +02:00
Steve Repsher
1f1898fa46 Make module types explicit and convert some to ESM (#15964) 2023-03-30 11:23:43 +02:00
renovate[bot]
fcc95825e3 Update dependency sinon to v15.0.3 (#15972) 2023-03-29 21:03:33 -04:00
108 changed files with 1244 additions and 850 deletions

View File

@@ -20,7 +20,7 @@
"settings": {
"import/resolver": {
"webpack": {
"config": "./webpack.config.js"
"config": "./webpack.config.cjs"
}
}
},

View File

@@ -43,7 +43,7 @@ jobs:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Bump version
run: script/version_bump.js nightly
run: script/version_bump.cjs nightly
- name: Build nightly Python wheels
run: |

View File

@@ -1,6 +1,6 @@
const path = require("path");
const env = require("./env.js");
const paths = require("./paths.js");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
// GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -99,7 +99,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.js"
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
),
{
modules: ["@mdi/js"],

View File

@@ -1,6 +1,6 @@
const fs = require("fs");
const path = require("path");
const paths = require("./paths.js");
const paths = require("./paths.cjs");
module.exports = {
useRollup() {

View File

@@ -1,18 +1,18 @@
// Run HA develop mode
const gulp = require("gulp");
const env = require("../env");
require("./clean.js");
require("./translations.js");
require("./locale-data.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./compress.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
require("./wds.js");
const env = require("../env.cjs");
require("./clean.cjs");
require("./translations.cjs");
require("./locale-data.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./compress.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
require("./wds.cjs");
gulp.task(
"develop-app",

View File

@@ -1,14 +1,13 @@
const gulp = require("gulp");
const env = require("../env.cjs");
const env = require("../env");
require("./clean.js");
require("./translations.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
require("./clean.cjs");
require("./translations.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
gulp.task(
"develop-cast",

View File

@@ -1,7 +1,7 @@
const del = import("del");
const gulp = require("gulp");
const paths = require("../paths");
require("./translations");
const paths = require("../paths.cjs");
require("./translations.cjs");
gulp.task(
"clean",

View File

@@ -4,7 +4,7 @@ const gulp = require("gulp");
const zopfli = require("gulp-zopfli-green");
const merge = require("merge-stream");
const path = require("path");
const paths = require("../paths");
const paths = require("../paths.cjs");
const zopfliOptions = { threshold: 150 };

View File

@@ -1,16 +1,15 @@
// Run demo develop mode
const gulp = require("gulp");
const env = require("../env.cjs");
const env = require("../env");
require("./clean.js");
require("./translations.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
require("./clean.cjs");
require("./translations.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
gulp.task(
"develop-demo",

View File

@@ -4,9 +4,9 @@ const fs = require("fs-extra");
const path = require("path");
const template = require("lodash.template");
const { minify } = require("html-minifier-terser");
const paths = require("../paths.js");
const env = require("../env.js");
const { htmlMinifierOptions, terserOptions } = require("../bundle.js");
const paths = require("../paths.cjs");
const env = require("../env.cjs");
const { htmlMinifierOptions, terserOptions } = require("../bundle.cjs");
const templatePath = (tpl) =>
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`);

View File

@@ -6,17 +6,17 @@ const { marked } = require("marked");
const glob = require("glob");
const yaml = require("js-yaml");
const env = require("../env");
const paths = require("../paths");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
require("./clean.js");
require("./translations.js");
require("./gen-icons-json.js");
require("./gather-static.js");
require("./webpack.js");
require("./service-worker.js");
require("./entry-html.js");
require("./rollup.js");
require("./clean.cjs");
require("./translations.cjs");
require("./gen-icons-json.cjs");
require("./gather-static.cjs");
require("./webpack.cjs");
require("./service-worker.cjs");
require("./entry-html.cjs");
require("./rollup.cjs");
gulp.task("gather-gallery-pages", async function gatherPages() {
const pageDir = path.resolve(paths.gallery_dir, "src/pages");
@@ -89,9 +89,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
// Generate sidebar
const sidebarPath = path.resolve(paths.gallery_dir, "sidebar.js");
// To make watch work during development
delete require.cache[sidebarPath];
const sidebar = require(sidebarPath);
const sidebar = (await import(sidebarPath)).default;
const pagesToProcess = {};
for (const key of processed) {

View File

@@ -3,7 +3,7 @@
const gulp = require("gulp");
const path = require("path");
const fs = require("fs-extra");
const paths = require("../paths");
const paths = require("../paths.cjs");
const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts);

View File

@@ -134,11 +134,11 @@ gulp.task("gen-icons-json", (done) => {
});
const file = fs.readFileSync(PACKAGE_PATH, { encoding });
const package = JSON.parse(file);
const packageMeta = JSON.parse(file);
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "iconMetadata.json"),
JSON.stringify({ version: package.version, parts })
JSON.stringify({ version: packageMeta.version, parts })
);
fs.writeFileSync(

View File

@@ -1,13 +1,13 @@
const gulp = require("gulp");
const env = require("../env");
require("./clean.js");
require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
require("./gen-icons-json.js");
const env = require("../env.cjs");
require("./clean.cjs");
require("./compress.cjs");
require("./entry-html.cjs");
require("./gather-static.cjs");
require("./gen-icons-json.cjs");
require("./rollup.cjs");
require("./translations.cjs");
require("./webpack.cjs");
gulp.task(
"develop-hassio",

View File

@@ -2,7 +2,7 @@ const del = import("del");
const path = require("path");
const gulp = require("gulp");
const fs = require("fs");
const paths = require("../paths");
const paths = require("../paths.cjs");
const outDir = "build/locale-data";

View File

@@ -6,8 +6,8 @@ const handler = require("serve-handler");
const http = require("http");
const log = require("fancy-log");
const open = require("open");
const rollupConfig = require("../rollup");
const paths = require("../paths");
const rollupConfig = require("../rollup.cjs");
const paths = require("../paths.cjs");
const bothBuilds = (createConfigFunc, params) =>
gulp.series(
@@ -46,7 +46,7 @@ function createServer(serveOptions) {
);
}
function watchRollup(createConfig, extraWatchSrc = [], serveOptions) {
function watchRollup(createConfig, extraWatchSrc = [], serveOptions = null) {
const { inputOptions, outputOptions } = createConfig({
isProdBuild: false,
latestBuild: true,

View File

@@ -5,7 +5,7 @@ const path = require("path");
const fs = require("fs-extra");
const workboxBuild = require("workbox-build");
const sourceMapUrl = require("source-map-url");
const paths = require("../paths.js");
const paths = require("../paths.cjs");
const swDest = path.resolve(paths.app_output_root, "service_worker.js");

View File

@@ -9,11 +9,11 @@ const flatmap = require("gulp-flatmap");
const merge = require("gulp-merge-json");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const { mapFiles } = require("../util");
const env = require("../env");
const paths = require("../paths");
const { mapFiles } = require("../util.cjs");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
require("./fetch-nightly-translations");
require("./fetch-nightly-translations.cjs");
const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend";

View File

@@ -5,15 +5,15 @@ const webpack = require("webpack");
const WebpackDevServer = require("webpack-dev-server");
const log = require("fancy-log");
const path = require("path");
const env = require("../env");
const paths = require("../paths");
const env = require("../env.cjs");
const paths = require("../paths.cjs");
const {
createAppConfig,
createDemoConfig,
createCastConfig,
createHassioConfig,
createGalleryConfig,
} = require("../webpack");
} = require("../webpack.cjs");
const bothBuilds = (createConfigFunc, params) => [
createConfigFunc({ ...params, latestBuild: true }),

View File

@@ -103,7 +103,7 @@ module.exports = function (opts = {}) {
}
delete optionsObject.type;
if (!new RegExp("^.*/").test(workerFile)) {
if (!/^.*\//.test(workerFile)) {
this.warn(
`Paths passed to the Worker constructor must be relative or absolute, i.e. start with /, ./ or ../ (just like dynamic import!). Ignoring "${workerFile}".`
);

View File

@@ -3,18 +3,18 @@ const path = require("path");
const commonjs = require("@rollup/plugin-commonjs");
const resolve = require("@rollup/plugin-node-resolve");
const json = require("@rollup/plugin-json");
const babel = require("@rollup/plugin-babel").babel;
const { babel } = require("@rollup/plugin-babel");
const replace = require("@rollup/plugin-replace");
const visualizer = require("rollup-plugin-visualizer");
const { string } = require("rollup-plugin-string");
const { terser } = require("rollup-plugin-terser");
const manifest = require("./rollup-plugins/manifest-plugin");
const worker = require("./rollup-plugins/worker-plugin");
const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin");
const ignore = require("./rollup-plugins/ignore-plugin");
const manifest = require("./rollup-plugins/manifest-plugin.cjs");
const worker = require("./rollup-plugins/worker-plugin.cjs");
const dontHashPlugin = require("./rollup-plugins/dont-hash-plugin.cjs");
const ignore = require("./rollup-plugins/ignore-plugin.cjs");
const bundle = require("./bundle");
const paths = require("./paths");
const bundle = require("./bundle.cjs");
const paths = require("./paths.cjs");
const extensions = [".js", ".ts"];

View File

@@ -4,8 +4,8 @@ const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
class LogStartCompilePlugin {
ignoredFirst = false;
@@ -64,6 +64,9 @@ const createWebpackConfig = ({
cacheCompression: false,
},
},
resolve: {
fullySpecified: false,
},
},
{
test: /\.css$/,
@@ -149,14 +152,17 @@ const createWebpackConfig = ({
},
},
output: {
filename: ({ chunk }) => {
if (!isProdBuild || isStatsBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
filename: ({ chunk }) =>
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
? "[name].js"
: "[name]-[contenthash].js",
chunkFilename:
isProdBuild && !isStatsBuild ? "[chunkhash:8].js" : "[id].chunk.js",
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
assetModuleFilename:
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
hashFunction: "xxhash64",
hashDigest: "base64url",
hashDigestLength: 11, // full length of 64 bit base64url
path: outputPath,
publicPath,
// To silence warning in worker plugin

View File

@@ -1,5 +1,5 @@
const rollup = require("../build-scripts/rollup.js");
const env = require("../build-scripts/env.js");
import rollup from "../build-scripts/rollup.cjs";
import env from "../build-scripts/env.cjs";
const config = rollup.createCastConfig({
isProdBuild: env.isProdBuild(),
@@ -7,4 +7,4 @@ const config = rollup.createCastConfig({
isStatsBuild: env.isStatsBuild(),
});
module.exports = { ...config.inputOptions, output: config.outputOptions };
export default { ...config.inputOptions, output: config.outputOptions };

View File

@@ -1,8 +1,8 @@
const { createCastConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
module.exports = createCastConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
export default webpack.createCastConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -1,5 +1,5 @@
const rollup = require("../build-scripts/rollup.js");
const env = require("../build-scripts/env.js");
import rollup from "../build-scripts/rollup.cjs";
import env from "../build-scripts/env.cjs";
const config = rollup.createDemoConfig({
isProdBuild: env.isProdBuild(),
@@ -7,4 +7,4 @@ const config = rollup.createDemoConfig({
isStatsBuild: env.isStatsBuild(),
});
module.exports = { ...config.inputOptions, output: config.outputOptions };
export default { ...config.inputOptions, output: config.outputOptions };

View File

@@ -1,12 +1,11 @@
const { createDemoConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
// File just used for stats builds
const latestBuild = true;
module.exports = createDemoConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
export default webpack.createDemoConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild,
});

View File

@@ -1,5 +1,5 @@
const rollup = require("../build-scripts/rollup.js");
const env = require("../build-scripts/env.js");
import rollup from "../build-scripts/rollup.cjs";
import env from "../build-scripts/env.cjs";
const config = rollup.createGalleryConfig({
isProdBuild: env.isProdBuild(),
@@ -7,4 +7,4 @@ const config = rollup.createGalleryConfig({
isStatsBuild: env.isStatsBuild(),
});
module.exports = { ...config.inputOptions, output: config.outputOptions };
export default { ...config.inputOptions, output: config.outputOptions };

View File

@@ -1,4 +1,4 @@
module.exports = [
export default [
{
// This section has no header and so all page links are shown directly in the sidebar
category: "concepts",

View File

@@ -1,8 +1,8 @@
const { createGalleryConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
module.exports = createGalleryConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
export default webpack.createGalleryConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -1,3 +1,13 @@
var requireDir = require("require-dir");
import { globIterate } from "glob";
requireDir("./build-scripts/gulp/");
const gulpImports = [];
for await (const gulpModule of globIterate("build-scripts/gulp/*.?(c|m)js", {
dotRelative: true,
})) {
gulpImports.push(import(gulpModule));
}
// Since all tasks are currently registered with gulp.task(), this is enough
// If any are converted to named exports, need to loop and aggregate exports here
await Promise.all(gulpImports);

View File

@@ -1,5 +1,5 @@
const rollup = require("../build-scripts/rollup.js");
const env = require("../build-scripts/env.js");
import rollup from "../build-scripts/rollup.cjs";
import env from "../build-scripts/env.cjs";
const config = rollup.createHassioConfig({
isProdBuild: env.isProdBuild(),
@@ -7,4 +7,4 @@ const config = rollup.createHassioConfig({
isStatsBuild: env.isStatsBuild(),
});
module.exports = { ...config.inputOptions, output: config.outputOptions };
export default { ...config.inputOptions, output: config.outputOptions };

View File

@@ -1,8 +1,8 @@
const { createHassioConfig } = require("../build-scripts/webpack.js");
const { isProdBuild, isStatsBuild } = require("../build-scripts/env.js");
import webpack from "../build-scripts/webpack.cjs";
import env from "../build-scripts/env.cjs";
module.exports = createHassioConfig({
isProdBuild: isProdBuild(),
isStatsBuild: isStatsBuild(),
export default webpack.createHassioConfig({
isProdBuild: env.isProdBuild(),
isStatsBuild: env.isStatsBuild(),
latestBuild: true,
});

View File

@@ -1,5 +1,5 @@
module.exports = {
"*.{js,ts}": ["prettier --write", "eslint --fix"],
export default {
"*.?(c|m){js,ts}": ["eslint --fix", "prettier --write"],
"!(/translations)*.{json,css,md,html}": "prettier --write",
"translations/*/*.json": (files) =>
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +

View File

@@ -19,10 +19,11 @@
"postinstall": "husky install",
"prepack": "pinst --disable",
"postpack": "pinst --enable",
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.js \"test/**/*.ts\""
"test": "instant-mocha --webpack-config ./test/webpack.config.js --require ./test/setup.cjs \"test/**/*.ts\""
},
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.4.2",
@@ -183,15 +184,15 @@
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.4",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.56.0",
"@web/dev-server": "0.1.36",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@web/dev-server": "0.1.37",
"@web/dev-server-rollup": "0.4.0",
"babel-loader": "9.1.2",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"del": "7.0.0",
"eslint": "8.36.0",
"eslint": "8.37.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
@@ -227,13 +228,12 @@
"open": "8.4.2",
"pinst": "3.0.0",
"prettier": "2.8.7",
"require-dir": "1.2.0",
"rollup": "2.79.1",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.0",
"serve-handler": "6.1.5",
"sinon": "15.0.2",
"sinon": "15.0.3",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"tar": "6.1.13",
@@ -254,7 +254,6 @@
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0"
},
"main": "src/home-assistant.js",
"prettier": {
"trailingComma": "es5",
"arrowParens": "always"

View File

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

View File

@@ -1,5 +1,5 @@
const rollup = require("./build-scripts/rollup.js");
const env = require("./build-scripts/env.js");
import rollup from "../build-scripts/rollup.cjs";
import env from "../build-scripts/env.cjs";
const config = rollup.createAppConfig({
isProdBuild: env.isProdBuild(),
@@ -7,4 +7,4 @@ const config = rollup.createAppConfig({
isStatsBuild: env.isStatsBuild(),
});
module.exports = { ...config.inputOptions, output: config.outputOptions };
export default { ...config.inputOptions, output: config.outputOptions };

View File

@@ -118,24 +118,40 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
"window",
],
},
device_tracker: {
source_type: ["bluetooth", "bluetooth_le", "gps", "router"],
},
fan: {
direction: ["forward", "reverse"],
},
humidifier: {
device_class: ["humidifier", "dehumidifier"],
},
media_player: {
device_class: ["tv", "speaker", "receiver"],
media_content_type: [
"album",
"app",
"artist",
"channel",
"channels",
"composer",
"contibuting_artist",
"episode",
"game",
"genre",
"image",
"movie",
"music",
"playlist",
"podcast",
"season",
"track",
"tvshow",
"url",
"video",
],
repeat: ["off", "one", "all"],
},
number: {
device_class: ["temperature"],

View File

@@ -302,6 +302,7 @@ export default class HaChartBase extends LitElement {
return css`
:host {
display: block;
position: relative;
}
.chartContainer {
overflow: hidden;

View File

@@ -61,6 +61,10 @@ class StateHistoryChartLine extends LitElement {
this._chartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "nearest",
axis: "x",
},
scales: {
x: {
type: "time",
@@ -108,7 +112,6 @@ class StateHistoryChartLine extends LitElement {
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
@@ -127,16 +130,13 @@ class StateHistoryChartLine extends LitElement {
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.1,
borderWidth: 1.5,
},
point: {
hitRadius: 5,
hitRadius: 50,
},
},
// @ts-expect-error

View File

@@ -102,6 +102,7 @@ class StatisticsChart extends LitElement {
if (
changedProps.has("statisticsData") ||
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend")
) {
this._generateData();
@@ -149,6 +150,10 @@ class StatisticsChart extends LitElement {
this._chartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "nearest",
axis: "x",
},
scales: {
x: {
type: "time",
@@ -186,7 +191,6 @@ class StatisticsChart extends LitElement {
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
label: (context) =>
`${context.dataset.label}: ${formatNumber(
@@ -208,9 +212,6 @@ class StatisticsChart extends LitElement {
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.4,
@@ -219,7 +220,7 @@ class StatisticsChart extends LitElement {
},
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
hitRadius: 50,
},
},
// @ts-expect-error
@@ -316,6 +317,7 @@ class StatisticsChart extends LitElement {
}
statDataSets.forEach((d, i) => {
if (
this.chartType === "line" &&
prevEndTime &&
prevValues &&
prevEndTime.getTime() !== start.getTime()

View File

@@ -25,8 +25,6 @@ export type ControlSelectOption = {
export class HaControlSelect extends LitElement {
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@property() public options?: ControlSelectOption[];
@property() public value?: string;
@@ -305,6 +303,14 @@ export class HaControlSelect extends LitElement {
justify-content: center;
flex-direction: column;
text-align: center;
padding: 2px;
width: 100%;
box-sizing: border-box;
}
.option .content span {
display: block;
width: 100%;
hyphens: auto;
}
:host([vertical]) {
width: var(--control-select-thickness);

View File

@@ -41,7 +41,9 @@ export class HaDialog extends DialogBase {
SUPPRESS_DEFAULT_PRESS_SELECTOR,
].join(", ");
this._updateScrolledAttribute();
this.contentElement?.addEventListener("scroll", this._onScroll);
this.contentElement?.addEventListener("scroll", this._onScroll, {
passive: true,
});
}
disconnectedCallback(): void {

View File

@@ -11,6 +11,9 @@ export class HaDrawer extends DrawerBase {
.mdc-drawer {
top: 0;
}
.mdc-drawer--modal.mdc-drawer--open {
left: min(0px, var(--drawer-modal-left-offset));
}
`,
];
}

View File

@@ -191,6 +191,9 @@ export class HaFileUpload extends LitElement {
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field__icon--trailing {
pointer-events: auto !important;
}
.dragged:before {
position: var(--layout-fit_-_position);
top: var(--layout-fit_-_top);

View File

@@ -33,7 +33,7 @@ export class HaHeaderBar extends LitElement {
unsafeCSS(topAppBarStyles),
css`
.mdc-top-app-bar__row {
height: var(--header-bar-height, 64px);
height: var(--header-height);
}
.mdc-top-app-bar {
position: static;

View File

@@ -92,7 +92,7 @@ export class HaSettingsRow extends LitElement {
::slotted(ha-switch) {
padding: 16px 0;
}
div[secondary] {
.secondary {
white-space: normal;
}
.prefix-wrap {

View File

@@ -10,6 +10,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
css`
.mdc-top-app-bar__row {
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.mdc-top-app-bar--fixed-adjust {
padding-top: var(--header-height);
@@ -21,7 +22,6 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
--app-header-background-color,
var(--mdc-theme-primary)
);
border-bottom: var(--app-header-border-bottom);
}
`,
];

View File

@@ -10,6 +10,7 @@ export class HaTopAppBar extends TopAppBarBase {
css`
.mdc-top-app-bar__row {
height: var(--header-height);
border-bottom: var(--app-header-border-bottom);
}
.mdc-top-app-bar--fixed-adjust {
padding-top: var(--header-height);
@@ -21,7 +22,6 @@ export class HaTopAppBar extends TopAppBarBase {
--app-header-background-color,
var(--mdc-theme-primary)
);
border-bottom: var(--app-header-border-bottom);
}
`,
];

View File

@@ -3,7 +3,9 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { UNAVAILABLE } from "./entity";
import { FrontendLocaleData } from "./translation";
export const enum CoverEntityFeature {
OPEN = 1,
@@ -106,3 +108,18 @@ interface CoverEntityAttributes extends HassEntityAttributeBase {
export interface CoverEntity extends HassEntityBase {
attributes: CoverEntityAttributes;
}
export function computeCoverPositionStateDisplay(
stateObj: CoverEntity,
locale: FrontendLocaleData,
position?: number
) {
const currentPosition =
position ??
stateObj.attributes.current_position ??
stateObj.attributes.current_tilt_position;
return currentPosition && currentPosition !== 100
? `${Math.round(currentPosition)}${blankBeforePercent(locale)}%`
: "";
}

View File

@@ -9,6 +9,8 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { FrontendLocaleData } from "./translation";
export const enum FanEntityFeature {
SET_SPEED = 1,
@@ -91,3 +93,15 @@ export function computeFanSpeedIcon(
: [mdiFanSpeed1, mdiFanSpeed2, mdiFanSpeed3][index - 1];
}
export const FAN_SPEED_COUNT_MAX_FOR_BUTTONS = 4;
export function computeFanSpeedStateDisplay(
stateObj: FanEntity,
locale: FrontendLocaleData,
speed?: number
) {
const currentSpeed = speed ?? stateObj.attributes.percentage;
return currentSpeed
? `${Math.round(currentSpeed)}${blankBeforePercent(locale)}%`
: "";
}

View File

@@ -1,5 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import { UiAction } from "../panels/lovelace/components/hui-action-editor";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntitySources } from "./entity_sources";
@@ -149,6 +151,7 @@ interface EntitySelectorFilter {
integration?: string;
domain?: string | readonly string[];
device_class?: string | readonly string[];
supported_features?: number | [number];
}
export interface EntitySelector {
@@ -358,6 +361,7 @@ export const filterSelectorEntities = (
const {
domain: filterDomain,
device_class: filterDeviceClass,
supported_features: filterSupportedFeature,
integration: filterIntegration,
} = filterEntity;
@@ -383,6 +387,16 @@ export const filterSelectorEntities = (
}
}
if (filterSupportedFeature) {
if (
ensureArray(filterSupportedFeature).some(
(feature) => !supportsFeature(entity, feature)
)
) {
return false;
}
}
if (
filterIntegration &&
entitySources?.[entity.entity_id]?.domain !== filterIntegration

View File

@@ -14,6 +14,7 @@ interface PipelineRunStartEvent extends PipelineEventBase {
language: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
};
}
@@ -40,7 +41,7 @@ interface PipelineSTTStartEvent extends PipelineEventBase {
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
text: string;
stt_output: { text: string };
};
}

View File

@@ -1,9 +1,7 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-control-select";
@@ -14,6 +12,7 @@ import {
AlarmMode,
ALARM_MODES,
} from "../../../../data/alarm_control_panel";
import { UNAVAILABLE } from "../../../../data/entity";
import { HomeAssistant } from "../../../../types";
import { showEnterCodeDialogDialog } from "./show-enter-code-dialog";
@@ -33,14 +32,10 @@ export class HaMoreInfoAlarmControlPanelModes extends LitElement {
});
});
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
super.updated(changedProp);
if (changedProp.has("stateObj") && this.stateObj) {
const oldStateObj = changedProp.get("stateObj") as HassEntity | undefined;
if (!oldStateObj || this.stateObj.state !== oldStateObj.state) {
this._currentMode = this._getCurrentMode(this.stateObj);
}
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (changedProp.has("stateObj")) {
this._currentMode = this._getCurrentMode(this.stateObj);
}
}
@@ -50,53 +45,59 @@ export class HaMoreInfoAlarmControlPanelModes extends LitElement {
);
}
private async _valueChanged(ev: CustomEvent) {
const mode = (ev.detail as any).value as AlarmMode;
const { state: modeState, service } = ALARM_MODES[mode];
if (modeState === this.stateObj.state) return;
// Force ha-control-select to previous mode because we don't known if the service call will succeed due to code check
this._currentMode = mode;
await this.requestUpdate("_currentMode");
this._currentMode = this._getCurrentMode(this.stateObj!);
private async _setMode(mode: AlarmMode) {
const { service } = ALARM_MODES[mode];
let code: string | undefined;
if (
(mode !== "disarmed" &&
this.stateObj.attributes.code_arm_required &&
this.stateObj.attributes.code_format) ||
(mode === "disarmed" && this.stateObj.attributes.code_format)
this.stateObj!.attributes.code_arm_required &&
this.stateObj!.attributes.code_format) ||
(mode === "disarmed" && this.stateObj!.attributes.code_format)
) {
const disarm = mode === "disarmed";
const response = await showEnterCodeDialogDialog(this, {
codeFormat: this.stateObj.attributes.code_format,
title: this.hass.localize(
codeFormat: this.stateObj!.attributes.code_format,
title: this.hass!.localize(
`ui.dialogs.more_info_control.alarm_control_panel.${
disarm ? "disarm_title" : "arm_title"
}`
),
submitText: this.hass.localize(
submitText: this.hass!.localize(
`ui.dialogs.more_info_control.alarm_control_panel.${
disarm ? "disarm_action" : "arm_action"
}`
),
});
if (!response) {
return;
if (response == null) {
throw new Error("cancel");
}
code = response;
}
await this.hass.callService("alarm_control_panel", service, {
await this.hass!.callService("alarm_control_panel", service, {
entity_id: this.stateObj!.entity_id,
code,
});
}
private async _valueChanged(ev: CustomEvent) {
const mode = (ev.detail as any).value as AlarmMode;
if (ALARM_MODES[mode].state === this.stateObj!.state) return;
const oldMode = this._getCurrentMode(this.stateObj!);
this._currentMode = mode;
try {
await this._setMode(mode);
} catch (err) {
this._currentMode = oldMode;
}
}
protected render() {
const color = stateColorCss(this.stateObj);
@@ -116,16 +117,14 @@ export class HaMoreInfoAlarmControlPanelModes extends LitElement {
.options=${options}
.value=${this._currentMode}
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.alarm_control_panel.modes_label"
)}
style=${styleMap({
"--control-select-color": color,
"--modes-count": modes.length.toString(),
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-select>
`;

View File

@@ -108,6 +108,7 @@ export class HaMoreInfoFanSpeed extends LitElement {
style=${styleMap({
"--control-select-color": color,
})}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
</ha-control-select>
`;
@@ -116,9 +117,9 @@ export class HaMoreInfoFanSpeed extends LitElement {
return html`
<ha-control-slider
vertical
.value=${this.value}
min="0"
max="100"
.value=${this.value}
.step=${this.stateObj.attributes.percentage_step ?? 1}
@value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay(

View File

@@ -31,7 +31,7 @@ class MoreInfoAlarmControlPanel extends LitElement {
"ui.dialogs.more_info_control.alarm_control_panel.disarm_action"
),
});
if (!response) {
if (response == null) {
return;
}
code = response;

View File

@@ -10,9 +10,12 @@ import {
import { customElement, property, state } from "lit/decorators";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import "../../../components/ha-attributes";
import { CoverEntity, CoverEntityFeature } from "../../../data/cover";
import {
computeCoverPositionStateDisplay,
CoverEntity,
CoverEntityFeature,
} from "../../../data/cover";
import type { HomeAssistant } from "../../../types";
import "../components/cover/ha-more-info-cover-buttons";
import "../components/cover/ha-more-info-cover-position";
@@ -27,7 +30,9 @@ class MoreInfoCover extends LitElement {
@property({ attribute: false }) public stateObj?: CoverEntity;
@state() private _displayedPosition?: number;
@state() private _livePosition?: number;
@state() private _liveTilt?: number;
@state() private _mode?: "position" | "button";
@@ -35,20 +40,29 @@ class MoreInfoCover extends LitElement {
this._mode = this._mode === "position" ? "button" : "position";
}
private _positionChanged(ev) {
private _positionSliderMoved(ev) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this._displayedPosition = value;
this._livePosition = value;
}
private _positionValueChanged() {
this._livePosition = undefined;
}
private _tiltSliderMoved(ev) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this._liveTilt = value;
}
private _tiltValueChanged() {
this._liveTilt = undefined;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("stateObj") && this.stateObj) {
if (supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION)) {
const currentPosition = this.stateObj?.attributes.current_position;
this._displayedPosition =
currentPosition != null ? Math.round(currentPosition) : undefined;
}
if (!this._mode) {
this._mode =
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
@@ -60,29 +74,29 @@ class MoreInfoCover extends LitElement {
}
private get _stateOverride() {
if (this._displayedPosition == null) return undefined;
const liveValue = this._livePosition ?? this._liveTilt;
const tempState = {
...this.stateObj,
state: this._displayedPosition ? "open" : "closed",
attributes: {
...this.stateObj!.attributes,
current_position: this._displayedPosition,
},
} as CoverEntity;
const forcedState =
liveValue != null ? (liveValue ? "open" : "closed") : undefined;
const stateDisplay = computeStateDisplay(
this.hass.localize,
tempState!,
this.stateObj!,
this.hass.locale,
this.hass.entities
this.hass.entities,
forcedState
);
return this._displayedPosition && this._displayedPosition !== 100
? `${stateDisplay} - ${Math.round(
this._displayedPosition
)}${blankBeforePercent(this.hass!.locale)}%`
: stateDisplay;
const positionStateDisplay = computeCoverPositionStateDisplay(
this.stateObj!,
this.hass.locale,
liveValue
);
if (positionStateDisplay) {
return `${stateDisplay}${positionStateDisplay}`;
}
return stateDisplay;
}
protected render() {
@@ -133,7 +147,8 @@ class MoreInfoCover extends LitElement {
<ha-more-info-cover-position
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._positionChanged}
@slider-moved=${this._positionSliderMoved}
@value-changed=${this._positionValueChanged}
></ha-more-info-cover-position>
`
: nothing}
@@ -142,6 +157,8 @@ class MoreInfoCover extends LitElement {
<ha-more-info-cover-tilt-position
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._tiltSliderMoved}
@value-changed=${this._tiltValueChanged}
></ha-more-info-cover-tilt-position>
`
: nothing}

View File

@@ -22,11 +22,12 @@ import {
computeAttributeNameDisplay,
computeAttributeValueDisplay,
} from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import "../../../components/ha-attributes";
import { UNAVAILABLE } from "../../../data/entity";
import {
computeFanSpeedStateDisplay,
computeFanSpeedCount,
FanEntity,
FanEntityFeature,
@@ -49,12 +50,16 @@ class MoreInfoFan extends LitElement {
@state() public _presetMode?: string;
@state() private _selectedPercentage?: number;
@state() private _liveSpeed?: number;
private _percentageChanged(ev) {
private _speedSliderMoved(ev) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
this._selectedPercentage = value;
this._liveSpeed = value;
}
private _speedValueChanged() {
this._liveSpeed = undefined;
}
private _toggle = () => {
@@ -107,12 +112,35 @@ class MoreInfoFan extends LitElement {
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("stateObj")) {
this._presetMode = this.stateObj?.attributes.preset_mode;
this._selectedPercentage = this.stateObj?.attributes.percentage
? Math.round(this.stateObj.attributes.percentage)
: undefined;
}
}
private get _stateOverride() {
const liveValue = this._liveSpeed;
const forcedState =
this._liveSpeed != null ? (this._liveSpeed ? "on" : "off") : undefined;
const stateDisplay = computeStateDisplay(
this.hass.localize,
this.stateObj!,
this.hass.locale,
this.hass.entities,
forcedState
);
const positionStateDisplay = computeFanSpeedStateDisplay(
this.stateObj!,
this.hass.locale,
liveValue
);
if (positionStateDisplay) {
return positionStateDisplay;
}
return stateDisplay;
}
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
@@ -140,17 +168,11 @@ class MoreInfoFan extends LitElement {
supportsSpeed &&
computeFanSpeedCount(this.stateObj) > FAN_SPEED_COUNT_MAX_FOR_BUTTONS;
const stateOverride = this._selectedPercentage
? `${Math.round(this._selectedPercentage)}${blankBeforePercent(
this.hass!.locale
)}%`
: undefined;
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateOverride=${stateOverride}
.stateOverride=${this._stateOverride}
></ha-more-info-state-header>
<div class="controls">
${
@@ -159,7 +181,8 @@ class MoreInfoFan extends LitElement {
<ha-more-info-fan-speed
.stateObj=${this.stateObj}
.hass=${this.hass}
@slider-moved=${this._percentageChanged}
@slider-moved=${this._speedSliderMoved}
@value-changed=${this._speedValueChanged}
>
</ha-more-info-fan-speed>
`

View File

@@ -478,7 +478,7 @@ export class MoreInfoDialog extends LitElement {
@media all and (max-width: 450px) {
.child-view > * {
min-height: calc(100vh - 56px);
min-height: calc(100vh - var(--header-height));
}
}

View File

@@ -90,7 +90,7 @@ export class MoreInfoInfo extends LitElement {
@media all and (max-width: 450px) {
.container {
min-height: calc(100vh - 56px);
min-height: calc(100vh - var(--header-height));
}
}

View File

@@ -152,7 +152,6 @@ export class HuiNotificationDrawer extends LitElement {
--mdc-theme-primary: var(--primary-background-color);
border-bottom: 1px solid var(--divider-color);
display: block;
--header-bar-height: var(--header-height);
}
.notifications {

View File

@@ -755,6 +755,9 @@ export class QuickBar extends LitElement {
haStyleScrollbar,
haStyleDialog,
css`
mwc-list {
--mdc-list-vertical-padding: 0;
}
.heading {
display: flex;
align-items: center;

View File

@@ -44,6 +44,25 @@ const handleExternalMessage = (
success: true,
result: null,
});
} else if (msg.command === "sidebar/toggle") {
fireEvent(hassMainEl, "hass-toggle-menu");
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else if (msg.command === "sidebar/show") {
fireEvent(hassMainEl, "hass-toggle-menu", {
open: true,
screenPercentage: msg.data?.screenPercentage,
});
bus.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else {
return false;
}

View File

@@ -121,9 +121,24 @@ interface EMIncomingMessageShowNotifications {
command: "notifications/show";
}
interface EMIncomingMessageToggleSidebar {
id: number;
type: "command";
command: "sidebar/toggle";
}
interface EMIncomingMessageShowSidebar {
id: number;
type: "command";
command: "sidebar/show";
data?: { screenPercentage: number };
}
export type EMIncomingMessageCommands =
| EMIncomingMessageRestart
| EMIncomingMessageShowNotifications;
| EMIncomingMessageShowNotifications
| EMIncomingMessageToggleSidebar
| EMIncomingMessageShowSidebar;
type EMIncomingMessage =
| EMMessageResultSuccess

View File

@@ -1,8 +1,6 @@
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover, initial-scale=1'>
<style>
html {
overflow: hidden;
}
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;

View File

@@ -65,7 +65,7 @@ class HassErrorScreen extends LitElement {
align-items: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
padding: 8px 12px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
@@ -73,6 +73,11 @@ class HassErrorScreen extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
ha-icon-button-arrow-prev {
pointer-events: auto;
}

View File

@@ -60,7 +60,7 @@ class HassLoadingScreen extends LitElement {
align-items: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
padding: 8px 12px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
@@ -68,6 +68,11 @@ class HassLoadingScreen extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
ha-menu-button,
ha-icon-button-arrow-prev {
pointer-events: auto;

View File

@@ -111,7 +111,7 @@ class HassSubpage extends LitElement {
align-items: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
padding: 8px 12px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
@@ -119,6 +119,11 @@ class HassSubpage extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;

View File

@@ -323,7 +323,6 @@ export class HaTabsSubpageDataTable extends LitElement {
--text-field-overflow: initial;
display: flex;
justify-content: flex-end;
margin-right: 8px;
color: var(--primary-text-color);
}
.active-filters {

View File

@@ -235,9 +235,14 @@ class HassTabsSubpage extends LitElement {
background-color: var(--sidebar-background-color);
font-weight: 400;
border-bottom: 1px solid var(--divider-color);
padding: 0 16px;
padding: 8px 12px;
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
.toolbar a {
color: var(--sidebar-text-color);
text-decoration: none;

View File

@@ -7,11 +7,11 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import "@material/mwc-drawer/mwc-drawer";
import { customElement, property, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../common/dom/fire_event";
import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import "../components/ha-drawer";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver";
@@ -19,12 +19,15 @@ import "./partial-panel-resolver";
declare global {
// for fire event
interface HASSDomEvents {
"hass-toggle-menu": undefined;
"hass-toggle-menu":
| undefined
| { open?: boolean; screenPercentage?: number };
"hass-edit-sidebar": EditSideBarEvent;
"hass-show-notifications": undefined;
}
interface HTMLElementEventMap {
"hass-edit-sidebar": HASSDomEvent<EditSideBarEvent>;
"hass-toggle-menu": HASSDomEvent<HASSDomEvents["hass-toggle-menu"]>;
}
}
@@ -57,7 +60,7 @@ export class HomeAssistantMain extends LitElement {
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
return html`
<mwc-drawer
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : undefined}
@MDCDrawer:closed=${this._drawerClosed}
@@ -75,7 +78,7 @@ export class HomeAssistantMain extends LitElement {
.route=${this.route}
slot="appContent"
></partial-panel-resolver>
</mwc-drawer>
</ha-drawer>
`;
}
@@ -107,7 +110,7 @@ export class HomeAssistantMain extends LitElement {
}
);
this.addEventListener("hass-toggle-menu", () => {
this.addEventListener("hass-toggle-menu", (ev) => {
if (this._sidebarEditMode) {
return;
}
@@ -118,10 +121,20 @@ export class HomeAssistantMain extends LitElement {
return;
}
if (this._sidebarNarrow) {
this._drawerOpen = !this._drawerOpen;
this._drawerOpen = ev.detail?.open ?? !this._drawerOpen;
const offset = ev.detail?.screenPercentage
? -256 + screen.width * (ev.detail.screenPercentage / 100)
: 0;
this.style.setProperty("--drawer-modal-left-offset", `${offset}px`);
} else {
fireEvent(this, "hass-dock-sidebar", {
dock: this.hass.dockedSidebar === "auto" ? "docked" : "auto",
dock: ev.detail?.open
? "docked"
: ev.detail?.open === false
? "auto"
: this.hass.dockedSidebar === "auto"
? "docked"
: "auto",
});
}
});
@@ -142,10 +155,12 @@ export class HomeAssistantMain extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
toggleAttribute(this, "expanded", this.hass.dockedSidebar === "docked");
toggleAttribute(
this,
"expanded",
this.narrow || this.hass.dockedSidebar !== "auto"
"modal",
this._sidebarNarrow || this._externalSidebar
);
}
@@ -165,20 +180,20 @@ export class HomeAssistantMain extends LitElement {
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--mdc-drawer-width: 56px;
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
}
:host([expanded]) {
--mdc-drawer-width: calc(256px + env(safe-area-inset-left));
}
:host([modal]) {
--mdc-drawer-width: unset;
--mdc-top-app-bar-width: unset;
}
partial-panel-resolver,
ha-sidebar {
/* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
@media (min-width: 870px) {
partial-panel-resolver {
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
}
}
`;
}
}

View File

@@ -511,7 +511,7 @@ export class HaAutomationTrace extends LitElement {
justify-content: center;
font-size: 20px;
height: var(--header-height);
padding: 0 16px;
padding: 4px;
background-color: var(--primary-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
@@ -520,7 +520,7 @@ export class HaAutomationTrace extends LitElement {
}
.main {
height: calc(100% - 56px);
height: calc(100% - var(--header-height));
display: flex;
background-color: var(--card-background-color);
direction: ltr;

View File

@@ -376,7 +376,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
);
const filterMenu = html`
<div slot=${ifDefined(this.narrow ? "toolbar-icon" : "suffix")}>
<div slot=${ifDefined(this.narrow ? "toolbar-icon" : undefined)}>
<div class="menu-badge-container">
${!this._showDisabled && this.narrow && disabledCount
? html`<span class="badge">${disabledCount}</span>`
@@ -455,24 +455,25 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
)}
>
${!this._showDisabled && disabledCount
? html`<div
class="active-filters"
slot="suffix"
@click=${this._preventDefault}
>
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
{ number: disabledCount }
)}
<mwc-button
@click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show"
? html`<div class="filters" slot="suffix">
<div
class="active-filters"
@click=${this._preventDefault}
>
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
{ number: disabledCount }
)}
></mwc-button>
<mwc-button
@click=${this._toggleShowDisabled}
.label=${this.hass.localize(
"ui.panel.config.integrations.disable.show"
)}
></mwc-button>
</div>
${filterMenu}
</div>`
: ""}
${filterMenu}
</search-input>
</div>
`}
@@ -845,7 +846,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.container > * {
max-width: 500px;
}
.empty-message {
margin: auto;
text-align: center;
@@ -884,6 +884,15 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
right: 0;
left: 0;
}
.filters {
--mdc-text-field-fill-color: var(--input-fill-color);
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
--mdc-shape-small: 4px;
--text-field-overflow: initial;
display: flex;
justify-content: flex-end;
color: var(--primary-text-color);
}
.active-filters {
color: var(--primary-text-color);
position: relative;

View File

@@ -1,317 +1,131 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../../components/ha-card";
import "../../../../../../components/ha-alert";
import { customElement, property, query, state } from "lit/decorators";
import "../../../../../../components/ha-button";
import "../../../../../../components/ha-circular-progress";
import "../../../../../../components/ha-expansion-panel";
import "../../../../../../components/ha-textfield";
import {
PipelineRun,
runPipelineFromText,
} from "../../../../../../data/voice_assistant";
import "../../../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import "../../../../../../components/ha-formfield";
import "../../../../../../components/ha-checkbox";
import { haStyle } from "../../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../../types";
import { formatNumber } from "../../../../../../common/number/format_number";
import { showPromptDialog } from "../../../../../../dialogs/generic/show-dialog-box";
const RUN_DATA = {
pipeline: "Pipeline",
language: "Language",
};
const STT_DATA = {
engine: "Engine",
};
const INTENT_DATA = {
engine: "Engine",
intent_input: "Input",
};
const TTS_DATA = {
engine: "Engine",
tts_input: "Input",
};
const STAGES: Record<PipelineRun["stage"], number> = {
ready: 0,
stt: 1,
intent: 2,
tts: 3,
done: 4,
error: 5,
};
const hasStage = (run: PipelineRun, stage: PipelineRun["stage"]) =>
STAGES[run.init_options.start_stage] <= STAGES[stage] &&
STAGES[stage] <= STAGES[run.init_options.end_stage];
const maybeRenderError = (
run: PipelineRun,
stage: string,
lastRunStage: string
) => {
if (run.stage !== "error" || lastRunStage !== stage) {
return "";
}
return html`<ha-alert alert-type="error">
${run.error!.message} (${run.error!.code})
</ha-alert>`;
};
const renderProgress = (
hass: HomeAssistant,
pipelineRun: PipelineRun,
stage: PipelineRun["stage"]
) => {
const startEvent = pipelineRun.events.find(
(ev) => ev.type === `${stage}-start`
);
const finishEvent = pipelineRun.events.find(
(ev) => ev.type === `${stage}-end`
);
if (!startEvent) {
return "";
}
if (pipelineRun.stage === "error") {
return html``;
}
if (!finishEvent) {
return html`<ha-circular-progress
size="tiny"
active
></ha-circular-progress>`;
}
const duration =
new Date(finishEvent.timestamp).getTime() -
new Date(startEvent.timestamp).getTime();
const durationString = formatNumber(duration / 1000, hass.locale, {
maximumFractionDigits: 2,
});
return html`${durationString}s ✅`;
};
const renderData = (data: Record<string, any>, keys: Record<string, string>) =>
Object.entries(keys).map(
([key, label]) =>
html`
<div class="row">
<div>${label}</div>
<div>${data[key]}</div>
</div>
`
);
const dataMinusKeysRender = (
data: Record<string, any>,
keys: Record<string, string>
) => {
const result = {};
let render = false;
for (const key in data) {
if (key in keys) {
continue;
}
render = true;
result[key] = data[key];
}
return render ? html`<pre>${JSON.stringify(result, null, 2)}</pre>` : "";
};
import "./assist-render-pipeline-run";
import type { HaCheckbox } from "../../../../../../components/ha-checkbox";
import type { HaTextField } from "../../../../../../components/ha-textfield";
import "../../../../../../components/ha-textfield";
@customElement("assist-pipeline-debug")
export class AssistPipelineDebug extends SubscribeMixin(LitElement) {
export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _pipelineRun?: PipelineRun;
@state() private _pipelineRuns: PipelineRun[] = [];
@state() private _stopRecording?: () => void;
@query("#continue-conversation")
private _continueConversationCheckbox!: HaCheckbox;
@query("#continue-conversation-text")
private _continueConversationTextField?: HaTextField;
private _audioBuffer?: Int16Array[];
protected render(): TemplateResult {
const lastRunStage: string = this._pipelineRun
? ["tts", "intent", "stt"].find(
(stage) => this._pipelineRun![stage] !== undefined
) || "ready"
: "ready";
@state() private _finished = false;
protected render(): TemplateResult {
return html`
<hass-subpage
.narrow=${this.narrow}
.hass=${this.hass}
header="Assist Pipeline"
>
${this._pipelineRuns.length > 0
? html`
<ha-button
slot="toolbar-icon"
@click=${this._clearConversation}
.disabled=${!this._finished}
>
Clear
</ha-button>
`
: ""}
<div class="content">
<div class="start-row">
<ha-button
raised
@click=${this._runTextPipeline}
.disabled=${this._pipelineRun &&
!["error", "done"].includes(this._pipelineRun.stage)}
>
Run Text Pipeline
</ha-button>
<ha-button
raised
@click=${this._runAudioPipeline}
.disabled=${this._pipelineRun &&
!["error", "done"].includes(this._pipelineRun.stage)}
>
Run Audio Pipeline
</ha-button>
${this._pipelineRuns.length === 0
? html`
<ha-button raised @click=${this._runTextPipeline}>
Run Text Pipeline
</ha-button>
<ha-button raised @click=${this._runAudioPipeline}>
Run Audio Pipeline
</ha-button>
`
: this._pipelineRuns[0].init_options.start_stage === "intent"
? html`
<ha-textfield
id="continue-conversation-text"
label="Response"
.disabled=${!this._finished}
@keydown=${this._handleContinueKeyDown}
></ha-textfield>
<ha-button
@click=${this._runTextPipeline}
.disabled=${!this._finished}
>
Send
</ha-button>
`
: html`
<ha-formfield label="Continue conversation">
<ha-checkbox
id="continue-conversation"
checked
></ha-checkbox>
</ha-formfield>
`}
</div>
${this._pipelineRun
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<div>Run</div>
<div>${this._pipelineRun.stage}</div>
</div>
${renderData(this._pipelineRun.run, RUN_DATA)}
</div>
</ha-card>
${maybeRenderError(this._pipelineRun, "ready", lastRunStage)}
${hasStage(this._pipelineRun, "stt")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Speech-to-Text</span>
${renderProgress(
this.hass,
this._pipelineRun,
"stt"
)}
</div>
${this._pipelineRun.stt
? html`
<div class="card-content">
${renderData(this._pipelineRun.stt, STT_DATA)}
${dataMinusKeysRender(
this._pipelineRun.stt,
STT_DATA
)}
</div>
`
: ""}
</div>
${this._pipelineRun.stage === "stt" &&
this._stopRecording
? html`
<div class="card-actions">
<ha-button @click=${this._stopRecording}>
Stop Recording
</ha-button>
</div>
`
: ""}
</ha-card>
`
: ""}
${maybeRenderError(this._pipelineRun, "stt", lastRunStage)}
${hasStage(this._pipelineRun, "intent")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Natural Language Processing</span>
${renderProgress(
this.hass,
this._pipelineRun,
"intent"
)}
</div>
${this._pipelineRun.intent
? html`
<div class="card-content">
${renderData(
this._pipelineRun.intent,
INTENT_DATA
)}
${dataMinusKeysRender(
this._pipelineRun.intent,
INTENT_DATA
)}
</div>
`
: ""}
</div>
</ha-card>
`
: ""}
${maybeRenderError(this._pipelineRun, "intent", lastRunStage)}
${hasStage(this._pipelineRun, "tts")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Text-to-Speech</span>
${renderProgress(
this.hass,
this._pipelineRun,
"tts"
)}
</div>
${this._pipelineRun.tts
? html`
<div class="card-content">
${renderData(this._pipelineRun.tts, TTS_DATA)}
</div>
`
: ""}
</div>
${this._pipelineRun?.tts?.tts_output
? html`
<div class="card-actions">
<ha-button @click=${this._playTTS}>
Play Audio
</ha-button>
</div>
`
: ""}
</ha-card>
`
: ""}
${maybeRenderError(this._pipelineRun, "tts", lastRunStage)}
<ha-card>
<ha-expansion-panel>
<span slot="header">Raw</span>
<pre>${JSON.stringify(this._pipelineRun, null, 2)}</pre>
</ha-expansion-panel>
</ha-card>
`
: ""}
${this._pipelineRuns.map((run) =>
run === null
? ""
: html`
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
></assist-render-pipeline-run>
`
)}
</div>
</hass-subpage>
`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (
!changedProperties.has("_pipelineRun") ||
!this._pipelineRun ||
this._pipelineRun.init_options.start_stage !== "stt"
!changedProperties.has("_pipelineRuns") ||
this._pipelineRuns.length === 0
) {
return;
}
if (this._pipelineRun.stage === "stt" && this._audioBuffer) {
const currentRun = this._pipelineRuns[0];
if (currentRun.init_options.start_stage !== "stt") {
if (["error", "done"].includes(currentRun.stage)) {
this._finished = true;
}
return;
}
if (currentRun.stage === "stt" && this._audioBuffer) {
// Send the buffer over the WS to the STT engine.
for (const buffer of this._audioBuffer) {
this._sendAudioChunk(buffer);
@@ -319,31 +133,70 @@ export class AssistPipelineDebug extends SubscribeMixin(LitElement) {
this._audioBuffer = undefined;
}
if (this._pipelineRun.stage !== "stt" && this._stopRecording) {
if (currentRun.stage !== "stt" && this._stopRecording) {
this._stopRecording();
}
if (currentRun.stage === "done") {
const url = currentRun.tts!.tts_output!.url;
const audio = new Audio(url);
audio.addEventListener("ended", () => {
if (this._continueConversationCheckbox.checked) {
this._runAudioPipeline();
} else {
this._finished = true;
}
});
audio.play();
} else if (currentRun.stage === "error") {
this._finished = true;
}
}
private get conversationId(): string | null {
return this._pipelineRuns.length === 0
? null
: this._pipelineRuns[0].intent?.intent_output?.conversation_id || null;
}
private async _runTextPipeline() {
const text = await showPromptDialog(this, {
title: "Input text",
confirmText: "Run",
});
const textfield = this._continueConversationTextField;
let text: string | null;
if (textfield) {
text = textfield.value;
} else {
text = await showPromptDialog(this, {
title: "Input text",
confirmText: "Run",
});
}
if (!text) {
return;
}
this._pipelineRun = undefined;
let added = false;
runPipelineFromText(
this.hass,
(run) => {
this._pipelineRun = run;
if (textfield && ["done", "error"].includes(run.stage)) {
textfield.value = "";
}
if (added) {
this._pipelineRuns = [run, ...this._pipelineRuns.slice(1)];
} else {
this._pipelineRuns = [run, ...this._pipelineRuns];
added = true;
}
},
{
start_stage: "intent",
end_stage: "intent",
input: { text },
conversation_id: this.conversationId,
}
);
}
@@ -375,21 +228,28 @@ export class AssistPipelineDebug extends SubscribeMixin(LitElement) {
this._audioBuffer.push(e.data);
return;
}
if (this._pipelineRun?.stage !== "stt") {
if (this._pipelineRuns[0].stage !== "stt") {
return;
}
this._sendAudioChunk(e.data);
};
this._pipelineRun = undefined;
this._finished = false;
let added = false;
runPipelineFromText(
this.hass,
(run) => {
this._pipelineRun = run;
if (added) {
this._pipelineRuns = [run, ...this._pipelineRuns.slice(1)];
} else {
this._pipelineRuns = [run, ...this._pipelineRuns];
added = true;
}
},
{
start_stage: "stt",
end_stage: "tts",
conversation_id: this.conversationId,
}
);
}
@@ -397,16 +257,20 @@ export class AssistPipelineDebug extends SubscribeMixin(LitElement) {
private _sendAudioChunk(chunk: Int16Array) {
// Turn into 8 bit so we can prefix our handler ID.
const data = new Uint8Array(1 + chunk.length * 2);
data[0] = this._pipelineRun!.run.runner_data.stt_binary_handler_id!;
data[0] = this._pipelineRuns[0].run.runner_data.stt_binary_handler_id!;
data.set(new Uint8Array(chunk.buffer), 1);
this.hass.connection.socket!.send(data);
}
private _playTTS(): void {
const url = this._pipelineRun!.tts!.tts_output!.url;
const audio = new Audio(url);
audio.play();
private _handleContinueKeyDown(ev) {
if (ev.keyCode === 13) {
this._runTextPipeline();
}
}
private _clearConversation() {
this._pipelineRuns = [];
}
static styles = [
@@ -419,32 +283,19 @@ export class AssistPipelineDebug extends SubscribeMixin(LitElement) {
direction: ltr;
}
.start-row {
text-align: center;
}
.start-row ha-button {
margin: 16px;
}
ha-card,
ha-alert {
display: block;
margin-bottom: 16px;
}
.run-pipeline-card ha-textfield {
display: block;
}
.row {
display: flex;
justify-content: space-between;
justify-content: space-around;
align-items: center;
margin: 0 16px 16px;
}
pre {
margin: 0;
.start-row ha-textfield {
flex: 1;
}
ha-expansion-panel {
padding-left: 8px;
assist-render-pipeline-run {
padding-top: 16px;
}
.heading {
font-weight: 500;
margin-bottom: 16px;
assist-render-pipeline-run + assist-render-pipeline-run {
border-top: 3px solid black;
}
`,
];

View File

@@ -0,0 +1,336 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../../../components/ha-card";
import "../../../../../../components/ha-alert";
import "../../../../../../components/ha-button";
import "../../../../../../components/ha-circular-progress";
import "../../../../../../components/ha-expansion-panel";
import type { PipelineRun } from "../../../../../../data/voice_assistant";
import type { HomeAssistant } from "../../../../../../types";
import { formatNumber } from "../../../../../../common/number/format_number";
const RUN_DATA = {
pipeline: "Pipeline",
language: "Language",
};
const STT_DATA = {
engine: "Engine",
};
const INTENT_DATA = {
engine: "Engine",
intent_input: "Input",
};
const TTS_DATA = {
engine: "Engine",
tts_input: "Input",
};
const STAGES: Record<PipelineRun["stage"], number> = {
ready: 0,
stt: 1,
intent: 2,
tts: 3,
done: 4,
error: 5,
};
const hasStage = (run: PipelineRun, stage: PipelineRun["stage"]) =>
STAGES[run.init_options.start_stage] <= STAGES[stage] &&
STAGES[stage] <= STAGES[run.init_options.end_stage];
const maybeRenderError = (
run: PipelineRun,
stage: string,
lastRunStage: string
) => {
if (run.stage !== "error" || lastRunStage !== stage) {
return "";
}
return html`<ha-alert alert-type="error">
${run.error!.message} (${run.error!.code})
</ha-alert>`;
};
const renderProgress = (
hass: HomeAssistant,
pipelineRun: PipelineRun,
stage: PipelineRun["stage"]
) => {
const startEvent = pipelineRun.events.find(
(ev) => ev.type === `${stage}-start`
);
const finishEvent = pipelineRun.events.find(
(ev) => ev.type === `${stage}-end`
);
if (!startEvent) {
return "";
}
if (pipelineRun.stage === "error") {
return html``;
}
if (!finishEvent) {
return html`<ha-circular-progress
size="tiny"
active
></ha-circular-progress>`;
}
const duration =
new Date(finishEvent.timestamp).getTime() -
new Date(startEvent.timestamp).getTime();
const durationString = formatNumber(duration / 1000, hass.locale, {
maximumFractionDigits: 2,
});
return html`${durationString}s ✅`;
};
const renderData = (data: Record<string, any>, keys: Record<string, string>) =>
Object.entries(keys).map(
([key, label]) =>
html`
<div class="row">
<div>${label}</div>
<div>${data[key]}</div>
</div>
`
);
const dataMinusKeysRender = (
data: Record<string, any>,
keys: Record<string, string>
) => {
const result = {};
let render = false;
for (const key in data) {
if (key in keys) {
continue;
}
render = true;
result[key] = data[key];
}
return render ? html`<pre>${JSON.stringify(result, null, 2)}</pre>` : "";
};
@customElement("assist-render-pipeline-run")
export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private pipelineRun!: PipelineRun;
protected render(): TemplateResult {
const lastRunStage: string = this.pipelineRun
? ["tts", "intent", "stt"].find(
(stage) => this.pipelineRun![stage] !== undefined
) || "ready"
: "ready";
const messages: Array<{ from: string; text: string }> = [];
const userMessage =
this.pipelineRun.init_options.input?.text ||
this.pipelineRun?.stt?.stt_output?.text;
if (userMessage) {
messages.push({
from: "user",
text: userMessage,
});
}
if (
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
) {
messages.push({
from: "hass",
text: this.pipelineRun.intent.intent_output.response.speech.plain
.speech,
});
}
return html`
<ha-card>
<div class="card-content">
<div class="row heading">
<div>Run</div>
<div>${this.pipelineRun.stage}</div>
</div>
${renderData(this.pipelineRun.run, RUN_DATA)}
${messages.length > 0
? html`
<div class="messages">
${messages.map(
({ from, text }) => html`
<div class=${`message ${from}`}>${text}</div>
`
)}
</div>
<div style="clear:both"></div>
`
: ""}
</div>
</ha-card>
${maybeRenderError(this.pipelineRun, "ready", lastRunStage)}
${hasStage(this.pipelineRun, "stt")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Speech-to-Text</span>
${renderProgress(this.hass, this.pipelineRun, "stt")}
</div>
${this.pipelineRun.stt
? html`
<div class="card-content">
${renderData(this.pipelineRun.stt, STT_DATA)}
${dataMinusKeysRender(this.pipelineRun.stt, STT_DATA)}
</div>
`
: ""}
</div>
</ha-card>
`
: ""}
${maybeRenderError(this.pipelineRun, "stt", lastRunStage)}
${hasStage(this.pipelineRun, "intent")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Natural Language Processing</span>
${renderProgress(this.hass, this.pipelineRun, "intent")}
</div>
${this.pipelineRun.intent
? html`
<div class="card-content">
${renderData(this.pipelineRun.intent, INTENT_DATA)}
${dataMinusKeysRender(
this.pipelineRun.intent,
INTENT_DATA
)}
</div>
`
: ""}
</div>
</ha-card>
`
: ""}
${maybeRenderError(this.pipelineRun, "intent", lastRunStage)}
${hasStage(this.pipelineRun, "tts")
? html`
<ha-card>
<div class="card-content">
<div class="row heading">
<span>Text-to-Speech</span>
${renderProgress(this.hass, this.pipelineRun, "tts")}
</div>
${this.pipelineRun.tts
? html`
<div class="card-content">
${renderData(this.pipelineRun.tts, TTS_DATA)}
</div>
`
: ""}
</div>
${this.pipelineRun?.tts?.tts_output
? html`
<div class="card-actions">
<ha-button @click=${this._playTTS}>
Play Audio
</ha-button>
</div>
`
: ""}
</ha-card>
`
: ""}
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
<ha-card>
<ha-expansion-panel>
<span slot="header">Raw</span>
<pre>${JSON.stringify(this.pipelineRun, null, 2)}</pre>
</ha-expansion-panel>
</ha-card>
`;
}
private _playTTS(): void {
const url = this.pipelineRun!.tts!.tts_output!.url;
const audio = new Audio(url);
audio.play();
}
static styles = css`
:host {
display: block;
}
ha-card,
ha-alert {
display: block;
margin-bottom: 16px;
}
.row {
display: flex;
justify-content: space-between;
}
pre {
margin: 0;
}
ha-expansion-panel {
padding-left: 8px;
}
.heading {
font-weight: 500;
margin-bottom: 16px;
}
.messages {
margin-top: 8px;
}
.message {
font-size: 18px;
margin: 8px 0;
padding: 8px;
border-radius: 15px;
clear: both;
}
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;
float: var(--float-start);
border-bottom-left-radius: 0px;
background-color: var(--primary-color);
color: var(--text-primary-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"assist-render-pipeline-run": AssistPipelineDebug;
}
}

View File

@@ -508,7 +508,7 @@ export class HaScriptTrace extends LitElement {
}
.main {
height: calc(100% - 56px);
height: calc(100% - var(--header-height));
display: flex;
background-color: var(--card-background-color);
}

View File

@@ -108,10 +108,15 @@ class PanelDeveloperTools extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
padding: 0 16px;
padding: 8px 12px;
font-weight: 400;
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 4px;
}
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;

View File

@@ -157,6 +157,9 @@ export class HuiEnergyGasGraphCard
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
@@ -171,9 +174,6 @@ export class HuiEnergyGasGraphCard
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
@@ -213,7 +213,7 @@ export class HuiEnergyGasGraphCard
},
plugins: {
tooltip: {
mode: "nearest",
position: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
@@ -244,13 +244,10 @@ export class HuiEnergyGasGraphCard
},
},
},
hover: {
mode: "nearest",
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
hitRadius: 50,
},
},
// @ts-expect-error

View File

@@ -154,6 +154,9 @@ export class HuiEnergySolarGraphCard
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
@@ -168,9 +171,6 @@ export class HuiEnergySolarGraphCard
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
@@ -209,7 +209,7 @@ export class HuiEnergySolarGraphCard
},
plugins: {
tooltip: {
mode: "nearest",
position: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
@@ -240,9 +240,6 @@ export class HuiEnergySolarGraphCard
},
},
},
hover: {
mode: "nearest",
},
elements: {
line: {
tension: 0.3,

View File

@@ -148,6 +148,9 @@ export class HuiEnergyUsageGraphCard
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
@@ -162,9 +165,6 @@ export class HuiEnergyUsageGraphCard
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
@@ -204,8 +204,6 @@ export class HuiEnergyUsageGraphCard
},
plugins: {
tooltip: {
mode: "x",
intersect: true,
position: "nearest",
filter: (val) => val.formattedValue !== "0",
callbacks: {
@@ -265,13 +263,10 @@ export class HuiEnergyUsageGraphCard
},
},
},
hover: {
mode: "nearest",
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
hitRadius: 50,
},
},
// @ts-expect-error

View File

@@ -157,6 +157,9 @@ export class HuiEnergyWaterGraphCard
const options: ChartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "x",
},
scales: {
x: {
type: "time",
@@ -171,9 +174,6 @@ export class HuiEnergyWaterGraphCard
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
@@ -213,7 +213,7 @@ export class HuiEnergyWaterGraphCard
},
plugins: {
tooltip: {
mode: "nearest",
position: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
@@ -244,13 +244,10 @@ export class HuiEnergyWaterGraphCard
},
},
},
hover: {
mode: "nearest",
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
hitRadius: 50,
},
},
// @ts-expect-error

View File

@@ -36,9 +36,12 @@ import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-image";
import "../../../components/tile/ha-tile-info";
import { cameraUrlWithWidthHeight } from "../../../data/camera";
import { CoverEntity } from "../../../data/cover";
import { isUnavailableState, ON } from "../../../data/entity";
import { FanEntity } from "../../../data/fan";
import {
computeCoverPositionStateDisplay,
CoverEntity,
} from "../../../data/cover";
import { isUnavailableState } from "../../../data/entity";
import { computeFanSpeedStateDisplay, FanEntity } from "../../../data/fan";
import { LightEntity } from "../../../data/light";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
@@ -202,7 +205,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
`;
}
if (domain === "light" && stateObj.state === ON) {
if (domain === "light" && stateActive(stateObj)) {
const brightness = (stateObj as LightEntity).attributes.brightness;
if (brightness) {
return `${Math.round((brightness * 100) / 255)}${blankBeforePercent(
@@ -211,10 +214,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
}
if (domain === "fan" && stateObj.state === ON) {
const speed = (stateObj as FanEntity).attributes.percentage;
if (speed) {
return `${Math.round(speed)}${blankBeforePercent(this.hass!.locale)}%`;
if (domain === "fan" && stateActive(stateObj)) {
const speedStateDisplay = computeFanSpeedStateDisplay(
stateObj as FanEntity,
this.hass!.locale
);
if (speedStateDisplay) {
return speedStateDisplay;
}
}
@@ -225,15 +231,14 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
this.hass!.entities
);
if (
domain === "cover" &&
["open", "opening", "closing"].includes(stateObj.state)
) {
const position = (stateObj as CoverEntity).attributes.current_position;
if (position && position !== 100) {
return `${stateDisplay} - ${Math.round(position)}${blankBeforePercent(
this.hass!.locale
)}%`;
if (domain === "cover" && stateActive(stateObj)) {
const positionStateDisplay = computeCoverPositionStateDisplay(
stateObj as CoverEntity,
this.hass!.locale
);
if (positionStateDisplay) {
return `${stateDisplay}${positionStateDisplay}`;
}
}
return stateDisplay;

View File

@@ -63,7 +63,7 @@ class ActionHandler extends HTMLElement implements ActionHandler {
public connectedCallback() {
Object.assign(this.style, {
position: "absolute",
position: "fixed",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)",
@@ -147,11 +147,11 @@ class ActionHandler extends HTMLElement implements ActionHandler {
let x;
let y;
if ((ev as TouchEvent).touches) {
x = (ev as TouchEvent).touches[0].pageX;
y = (ev as TouchEvent).touches[0].pageY;
x = (ev as TouchEvent).touches[0].clientX;
y = (ev as TouchEvent).touches[0].clientY;
} else {
x = (ev as MouseEvent).pageX;
y = (ev as MouseEvent).pageY;
x = (ev as MouseEvent).clientX;
y = (ev as MouseEvent).clientY;
}
if (options.hasHold) {

View File

@@ -164,7 +164,7 @@ export class HuiCreateDialogCard
ha-dialog {
--mdc-dialog-max-width: 845px;
--dialog-content-padding: 2px 24px 20px 24px;
--dialog-z-index: 5;
--dialog-z-index: 6;
}
ha-dialog.table {

View File

@@ -394,7 +394,7 @@ export class HuiDialogEditCard
ha-dialog {
--mdc-dialog-max-width: 845px;
--dialog-z-index: 5;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {

View File

@@ -143,7 +143,7 @@ export class HuiDialogSuggestCard extends LitElement {
}
ha-dialog {
max-width: 845px;
--dialog-z-index: 5;
--dialog-z-index: 6;
}
.hidden {
display: none;

View File

@@ -145,7 +145,7 @@ export class HuiCreateDialogHeaderFooter
ha-dialog {
--mdc-dialog-max-width: 550px;
--dialog-content-padding: 2px 24px 20px 24px;
--dialog-z-index: 5;
--dialog-z-index: 6;
}
.elements {

View File

@@ -26,7 +26,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
@@ -116,9 +122,9 @@ class HUIRoot extends LitElement {
return html`
<div
class=" ${classMap({
class=${classMap({
"edit-mode": this._editMode,
})}"
})}
>
<div class="header">
<div class="toolbar">
@@ -547,11 +553,20 @@ class HUIRoot extends LitElement {
`
: ""}
</div>
<div id="view" @ll-rebuild=${this._debouncedConfigChanged}></div>
<div
id="view"
@ll-rebuild=${this._debouncedConfigChanged}
@scroll=${this._viewScrolled}
></div>
</div>
`;
}
@eventOptions({ passive: true })
private _viewScrolled(ev) {
this.toggleAttribute("scrolled", ev.currentTarget.scrollTop !== 0);
}
private _isVisible = (view: LovelaceViewConfig) =>
Boolean(
this._editMode ||
@@ -947,6 +962,10 @@ class HUIRoot extends LitElement {
top: 0;
width: var(--mdc-top-app-bar-width, 100%);
z-index: 2;
transition: box-shadow 0.3s ease-out;
}
:host([scrolled]) .header {
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.75);
}
.edit-mode .header {
background-color: var(--app-header-edit-background-color, #455a64);
@@ -957,10 +976,15 @@ class HUIRoot extends LitElement {
display: flex;
align-items: center;
font-size: 20px;
padding: 0 16px;
padding: 0px 12px;
font-weight: 400;
box-sizing: border-box;
}
@media (max-width: 599px) {
.toolbar {
padding: 0 4px;
}
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
@@ -1025,14 +1049,10 @@ class HUIRoot extends LitElement {
100vh - var(--header-height) - env(safe-area-inset-top) -
env(safe-area-inset-bottom)
);
/**
* Since we only set min-height, if child nodes need percentage
* heights they must use absolute positioning so we need relative
* positioning here.
*
* https://www.w3.org/TR/CSS2/visudet.html#the-height-property
*/
position: relative;
background: var(
--lovelace-background,
var(--primary-background-color)
);
display: flex;
overflow: auto;
}
@@ -1064,12 +1084,6 @@ class HUIRoot extends LitElement {
.menu-link {
text-decoration: none;
}
hui-view {
background: var(
--lovelace-background,
var(--primary-background-color)
);
}
.exit-edit-mode {
--mdc-theme-primary: var(--app-header-edit-text-color, #fff);
--mdc-button-outline-color: var(--app-header-edit-text-color, #fff);

View File

@@ -1,23 +1,23 @@
import { mdiShieldOff } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-slider";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import {
AlarmControlPanelEntity,
AlarmMode,
ALARM_MODES,
} from "../../../data/alarm_control_panel";
import { UNAVAILABLE } from "../../../data/entity";
import { showEnterCodeDialogDialog } from "../../../dialogs/more-info/components/alarm_control_panel/show-enter-code-dialog";
import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types";
@@ -67,14 +67,10 @@ class HuiAlarmModeTileFeature
this._config = config;
}
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
super.updated(changedProp);
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (changedProp.has("stateObj") && this.stateObj) {
const oldStateObj = changedProp.get("stateObj") as HassEntity | undefined;
if (!oldStateObj || this.stateObj.state !== oldStateObj.state) {
this._currentMode = this._getCurrentMode(this.stateObj);
}
this._currentMode = this._getCurrentMode(this.stateObj);
}
}
@@ -108,12 +104,14 @@ class HuiAlarmModeTileFeature
if (ALARM_MODES[mode].state === this.stateObj!.state) return;
// Force ha-control-select to previous mode because we don't known if the service call will succeed due to code check
const oldMode = this._getCurrentMode(this.stateObj!);
this._currentMode = mode;
await this.requestUpdate("_currentMode");
this._currentMode = this._getCurrentMode(this.stateObj!);
this._setMode(mode);
try {
await this._setMode(mode);
} catch (err) {
this._currentMode = oldMode;
}
}
private async _disarm() {
@@ -146,13 +144,13 @@ class HuiAlarmModeTileFeature
}`
),
});
if (!response) {
return;
if (response == null) {
throw new Error("cancel");
}
code = response;
}
this.hass!.callService("alarm_control_panel", service, {
await this.hass!.callService("alarm_control_panel", service, {
entity_id: this.stateObj!.entity_id,
code,
});
@@ -201,16 +199,14 @@ class HuiAlarmModeTileFeature
.value=${this._currentMode}
@value-changed=${this._valueChanged}
hide-label
.label=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
.ariaLabel=${this.hass.localize(
"ui.dialogs.more_info_control.alarm_control_panel.modes_label"
)}
style=${styleMap({
"--control-select-color": color,
"--modes-count": modes.length.toString(),
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-select>
</div>

View File

@@ -100,12 +100,13 @@ class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
.value=${speed}
@value-changed=${this._speedValueChanged}
hide-label
.label=${computeAttributeNameDisplay(
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
)}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-select>
</div>
@@ -124,14 +125,14 @@ class HuiFanSpeedTileFeature extends LitElement implements LovelaceTileFeature {
min="0"
max="100"
.step=${this.stateObj.attributes.percentage_step ?? 1}
.disabled=${this.stateObj!.state === UNAVAILABLE}
@value-changed=${this._valueChanged}
.label=${computeAttributeNameDisplay(
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"percentage"
)}
.disabled=${this.stateObj!.state === UNAVAILABLE}
></ha-control-slider>
</div>
`;

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