20230426.0 (#16327)

This commit is contained in:
Bram Kragten 2023-04-26 18:31:51 +02:00 committed by GitHub
commit b7667d2cbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 9736 additions and 5841 deletions

21
.browserslistrc Normal file
View File

@ -0,0 +1,21 @@
[modern]
# Support for dynamic import is the main litmus test for serving modern builds.
# Although officially a ES2020 feature, browsers implemented it early, so this
# enables all of ES2017 and some features in ES2018.
supports es6-module-dynamic-import
# Exclude Safari 11-12 because of a bug in tagged template literals
# https://bugs.webkit.org/show_bug.cgi?id=190756
# Note: Dropping version 11 also enables several more ES2018 features
not Safari < 13
not iOS < 13
# Exclude unsupported browsers
not dead
[legacy]
# Legacy builds are transpiled to ES5 (strict mode) but also must support some features that cannot be polyfilled:
# - web sockets to communicate with backend
# - inline SVG used widely in buttons, widgets, etc.
# - custom events used for most user interactions
supports use-strict and supports websockets and supports svg-html5 and supports customevent

View File

@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
with: with:
ref: dev ref: dev
@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
with: with:
ref: master ref: master

View File

@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0
with: with:
@ -48,7 +48,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0
with: with:
@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0
with: with:
@ -84,7 +84,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0
with: with:

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@ -23,7 +23,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
with: with:
ref: dev ref: dev
@ -59,7 +59,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
with: with:
ref: master ref: master

View File

@ -17,7 +17,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0

View File

@ -22,7 +22,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Node ${{ env.NODE_VERSION }} - name: Set up Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v3.6.0 uses: actions/setup-node@v3.6.0

View File

@ -21,7 +21,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4

View File

@ -24,7 +24,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -75,7 +75,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@2022.10.1 uses: home-assistant/wheels@2023.04.0
with: with:
abi: cp310 abi: cp310
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.5.0 uses: actions/checkout@v3.5.2
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

@ -84,17 +84,23 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
babelrc: false, babelrc: false,
compact: false, compact: false,
assumptions: {
privateFieldsAsProperties: true,
setPublicClassFields: true,
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : "legacy",
presets: [ presets: [
!latestBuild && [ [
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: "entry", useBuiltIns: latestBuild ? false : "entry",
corejs: { version: "3.30", proposals: true }, corejs: latestBuild ? false : { version: "3.30", proposals: true },
bugfixes: true, bugfixes: true,
}, },
], ],
"@babel/preset-typescript", "@babel/preset-typescript",
].filter(Boolean), ],
plugins: [ plugins: [
[ [
path.resolve( path.resolve(
@ -106,22 +112,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
ignoreModuleNotFound: true, ignoreModuleNotFound: true,
}, },
], ],
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2}) // Support some proposals still in TC39 process
!latestBuild && [
"@babel/plugin-proposal-object-rest-spread",
{ loose: true, useBuiltIns: true },
],
// Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-top-level-await",
// Support various proposals
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
// Minify template literals for production // Minify template literals for production
isProdBuild && [ isProdBuild && [
"template-html-minifier", "template-html-minifier",

View File

@ -24,8 +24,7 @@ gulp.task(
gulp.parallel( gulp.parallel(
"gen-service-worker-app-dev", "gen-service-worker-app-dev",
"gen-icons-json", "gen-icons-json",
"gen-pages-dev", "gen-pages-app-dev",
"gen-index-app-dev",
"build-translations", "build-translations",
"build-locale-data" "build-locale-data"
), ),
@ -50,10 +49,6 @@ gulp.task(
env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app",
// Don't compress running tests // Don't compress running tests
...(env.isTestBuild() ? [] : ["compress-app"]), ...(env.isTestBuild() ? [] : ["compress-app"]),
gulp.parallel( gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod")
"gen-pages-prod",
"gen-index-app-prod",
"gen-service-worker-app-prod"
)
) )
); );

View File

@ -19,7 +19,7 @@ gulp.task(
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast", "copy-static-cast",
"gen-index-cast-dev", "gen-pages-cast-dev",
env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast" env.useRollup() ? "rollup-dev-server-cast" : "webpack-dev-server-cast"
) )
); );
@ -35,6 +35,6 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast", "copy-static-cast",
env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast", env.useRollup() ? "rollup-prod-cast" : "webpack-prod-cast",
"gen-index-cast-prod" "gen-pages-cast-prod"
) )
); );

View File

@ -21,7 +21,7 @@ gulp.task(
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel( gulp.parallel(
"gen-icons-json", "gen-icons-json",
"gen-index-demo-dev", "gen-pages-demo-dev",
"build-translations", "build-translations",
"build-locale-data" "build-locale-data"
), ),
@ -42,6 +42,6 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-demo", "copy-static-demo",
env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo", env.useRollup() ? "rollup-prod-demo" : "webpack-prod-demo",
"gen-index-demo-prod" "gen-pages-demo-prod"
) )
); );

View File

@ -8,344 +8,223 @@ const paths = require("../paths.cjs");
const env = require("../env.cjs"); const env = require("../env.cjs");
const { htmlMinifierOptions, terserOptions } = require("../bundle.cjs"); const { htmlMinifierOptions, terserOptions } = require("../bundle.cjs");
const templatePath = (tpl) => const renderTemplate = (templateFile, data = {}) => {
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`); const compiled = template(
fs.readFileSync(templateFile, { encoding: "utf-8" })
const readFile = (pth) => fs.readFileSync(pth).toString(); );
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
const compiled = template(readFile(pathFunc(pth)));
return compiled({ return compiled({
...data, ...data,
useRollup: env.useRollup(), useRollup: env.useRollup(),
useWDS: env.useWDS(), useWDS: env.useWDS(),
renderTemplate, // Resolve any child/nested templates relative to the parent and pass the same data
renderTemplate: (childTemplate) =>
renderTemplate(
path.resolve(path.dirname(templateFile), childTemplate),
data
),
}); });
}; };
const renderDemoTemplate = (pth, data = {}) => const WRAP_TAGS = { ".js": "script", ".css": "style" };
renderTemplate(pth, data, (tpl) =>
path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`)
);
const renderCastTemplate = (pth, data = {}) => const minifyHtml = (content, ext) => {
renderTemplate(pth, data, (tpl) => const wrapTag = WRAP_TAGS[ext] || "";
path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`) const begTag = wrapTag && `<${wrapTag}>`;
); const endTag = wrapTag && `</${wrapTag}>`;
return minify(begTag + content + endTag, {
const renderGalleryTemplate = (pth, data = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`)
);
const minifyHtml = (content) =>
minify(content, {
...htmlMinifierOptions, ...htmlMinifierOptions,
conservativeCollapse: false, conservativeCollapse: false,
minifyJS: terserOptions({ minifyJS: terserOptions({
latestBuild: false, // Shared scripts should be ES5 latestBuild: false, // Shared scripts should be ES5
isTestBuild: true, // Don't need source maps isTestBuild: true, // Don't need source maps
}), }),
}); }).then((wrapped) =>
wrapTag ? wrapped.slice(begTag.length, -endTag.length) : wrapped
);
};
const PAGES = ["onboarding", "authorize"]; // Function to generate a dev task for each project's configuration
// Note Currently WDS paths are hard-coded to only work for app
const genPagesDevTask =
(
pageEntries,
inputRoot,
outputRoot,
useWDS = false,
inputSub = "src/html",
publicRoot = ""
) =>
async () => {
for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate(
path.resolve(inputRoot, inputSub, `${page}.template`),
{
latestEntryJS: entries.map((entry) =>
useWDS
? `http://localhost:8000/src/entrypoints/${entry}.ts`
: `${publicRoot}/frontend_latest/${entry}.js`
),
es5EntryJS: entries.map(
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
),
latestCustomPanelJS: useWDS
? "http://localhost:8000/src/entrypoints/custom-panel.ts"
: `${publicRoot}/frontend_latest/custom-panel.js`,
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
}
);
fs.outputFileSync(path.resolve(outputRoot, page), content);
}
};
gulp.task("gen-pages-dev", (done) => { // Same as previous but for production builds
for (const page of PAGES) { // (includes minification and hashed file names from manifest)
const content = renderTemplate(page, { const genPagesProdTask =
latestPageJS: `/frontend_latest/${page}.js`, (
pageEntries,
es5PageJS: `/frontend_es5/${page}.js`, inputRoot,
}); outputRoot,
outputLatest,
fs.outputFileSync( outputES5,
path.resolve(paths.app_output_root, `${page}.html`), inputSub = "src/html"
content ) =>
); async () => {
} const latestManifest = require(path.resolve(outputLatest, "manifest.json"));
done(); const es5Manifest = outputES5
}); ? require(path.resolve(outputES5, "manifest.json"))
: {};
gulp.task("gen-pages-prod", async () => { const minifiedHTML = [];
const latestManifest = require(path.resolve( for (const [page, entries] of Object.entries(pageEntries)) {
paths.app_output_latest, const content = renderTemplate(
"manifest.json" path.resolve(inputRoot, inputSub, `${page}.template`),
)); {
const es5Manifest = require(path.resolve( latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
paths.app_output_es5, es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
"manifest.json" latestCustomPanelJS: latestManifest["custom-panel.js"],
)); es5CustomPanelJS: es5Manifest["custom-panel.js"],
}
const minifiedHTML = []; );
for (const page of PAGES) { minifiedHTML.push(
const content = renderTemplate(page, { minifyHtml(content, path.extname(page)).then((minified) =>
latestPageJS: latestManifest[`${page}.js`], fs.outputFileSync(path.resolve(outputRoot, page), minified)
es5PageJS: es5Manifest[`${page}.js`],
});
minifiedHTML.push(
minifyHtml(content).then((minified) =>
fs.outputFileSync(
path.resolve(paths.app_output_root, `${page}.html`),
minified
) )
) );
); }
} await Promise.all(minifiedHTML);
await Promise.all(minifiedHTML); };
});
gulp.task("gen-index-app-dev", (done) => { // Map HTML pages to their required entrypoints
let latestAppJS; const APP_PAGE_ENTRIES = {
let latestCoreJS; "authorize.html": ["authorize"],
let latestCustomPanelJS; "onboarding.html": ["onboarding"],
"index.html": ["core", "app"],
};
if (env.useWDS()) { gulp.task(
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts"; "gen-pages-app-dev",
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts"; genPagesDevTask(
latestCustomPanelJS = APP_PAGE_ENTRIES,
"http://localhost:8000/src/entrypoints/custom-panel.ts"; paths.polymer_dir,
} else { paths.app_output_root,
latestAppJS = "/frontend_latest/app.js"; env.useWDS()
latestCoreJS = "/frontend_latest/core.js"; )
latestCustomPanelJS = "/frontend_latest/custom-panel.js"; );
}
const content = renderTemplate("index", { gulp.task(
latestAppJS, "gen-pages-app-prod",
latestCoreJS, genPagesProdTask(
latestCustomPanelJS, APP_PAGE_ENTRIES,
paths.polymer_dir,
es5AppJS: "/frontend_es5/app.js", paths.app_output_root,
es5CoreJS: "/frontend_es5/core.js",
es5CustomPanelJS: "/frontend_es5/custom-panel.js",
}).replace(/#THEMEC/g, "{{ theme_color }}");
fs.outputFileSync(path.resolve(paths.app_output_root, "index.html"), content);
done();
});
gulp.task("gen-index-app-prod", async () => {
const latestManifest = require(path.resolve(
paths.app_output_latest, paths.app_output_latest,
"manifest.json" paths.app_output_es5
)); )
const es5Manifest = require(path.resolve( );
paths.app_output_es5,
"manifest.json"
));
const content = renderTemplate("index", {
latestAppJS: latestManifest["app.js"],
latestCoreJS: latestManifest["core.js"],
latestCustomPanelJS: latestManifest["custom-panel.js"],
es5AppJS: es5Manifest["app.js"], const CAST_PAGE_ENTRIES = {
es5CoreJS: es5Manifest["core.js"], "faq.html": ["launcher"],
es5CustomPanelJS: es5Manifest["custom-panel.js"], "index.html": ["launcher"],
}); "media.html": ["media"],
const minified = (await minifyHtml(content)).replace( "receiver.html": ["receiver"],
/#THEMEC/g, };
"{{ theme_color }}"
);
fs.outputFileSync( gulp.task(
path.resolve(paths.app_output_root, "index.html"), "gen-pages-cast-dev",
minified genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
); );
});
gulp.task("gen-index-cast-dev", (done) => { gulp.task(
const contentReceiver = renderCastTemplate("receiver", { "gen-pages-cast-prod",
latestReceiverJS: "/frontend_latest/receiver.js", genPagesProdTask(
}); CAST_PAGE_ENTRIES,
fs.outputFileSync( paths.cast_dir,
path.resolve(paths.cast_output_root, "receiver.html"), paths.cast_output_root,
contentReceiver
);
const contentMedia = renderCastTemplate("media", {
latestMediaJS: "/frontend_latest/media.js",
es5MediaJS: "/frontend_es5/media.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "faq.html"),
contentFAQ
);
const contentLauncher = renderCastTemplate("launcher", {
latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "index.html"),
contentLauncher
);
done();
});
gulp.task("gen-index-cast-prod", (done) => {
const latestManifest = require(path.resolve(
paths.cast_output_latest, paths.cast_output_latest,
"manifest.json" paths.cast_output_es5
)); )
const es5Manifest = require(path.resolve( );
paths.cast_output_es5,
"manifest.json"
));
const contentReceiver = renderCastTemplate("receiver", { const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
latestReceiverJS: latestManifest["receiver.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "receiver.html"),
contentReceiver
);
const contentMedia = renderCastTemplate("media", { gulp.task(
latestMediaJS: latestManifest["media.js"], "gen-pages-demo-dev",
es5MediaJS: es5Manifest["media.js"], genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
}); );
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", { gulp.task(
latestLauncherJS: latestManifest["launcher.js"], "gen-pages-demo-prod",
es5LauncherJS: es5Manifest["launcher.js"], genPagesProdTask(
}); DEMO_PAGE_ENTRIES,
fs.outputFileSync( paths.demo_dir,
path.resolve(paths.cast_output_root, "faq.html"), paths.demo_output_root,
contentFAQ
);
const contentLauncher = renderCastTemplate("launcher", {
latestLauncherJS: latestManifest["launcher.js"],
es5LauncherJS: es5Manifest["launcher.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "index.html"),
contentLauncher
);
done();
});
gulp.task("gen-index-demo-dev", (done) => {
const content = renderDemoTemplate("index", {
latestDemoJS: "/frontend_latest/main.js",
es5DemoJS: "/frontend_es5/main.js",
});
fs.outputFileSync(
path.resolve(paths.demo_output_root, "index.html"),
content
);
done();
});
gulp.task("gen-index-demo-prod", async () => {
const latestManifest = require(path.resolve(
paths.demo_output_latest, paths.demo_output_latest,
"manifest.json" paths.demo_output_es5
)); )
const es5Manifest = require(path.resolve( );
paths.demo_output_es5,
"manifest.json"
));
const content = renderDemoTemplate("index", {
latestDemoJS: latestManifest["main.js"],
es5DemoJS: es5Manifest["main.js"], const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
});
const minified = await minifyHtml(content);
fs.outputFileSync( gulp.task(
path.resolve(paths.demo_output_root, "index.html"), "gen-pages-gallery-dev",
minified genPagesDevTask(
); GALLERY_PAGE_ENTRIES,
}); paths.gallery_dir,
paths.gallery_output_root
)
);
gulp.task("gen-index-gallery-dev", (done) => { gulp.task(
const content = renderGalleryTemplate("index", { "gen-pages-gallery-prod",
latestGalleryJS: "./frontend_latest/entrypoint.js", genPagesProdTask(
}); GALLERY_PAGE_ENTRIES,
paths.gallery_dir,
paths.gallery_output_root,
paths.gallery_output_latest
)
);
fs.outputFileSync( const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
path.resolve(paths.gallery_output_root, "index.html"),
content
);
done();
});
gulp.task("gen-index-gallery-prod", async () => { gulp.task(
const latestManifest = require(path.resolve( "gen-pages-hassio-dev",
paths.gallery_output_latest, genPagesDevTask(
"manifest.json" HASSIO_PAGE_ENTRIES,
)); paths.hassio_dir,
const content = renderGalleryTemplate("index", { paths.hassio_output_root,
latestGalleryJS: latestManifest["entrypoint.js"], undefined,
}); "src",
const minified = await minifyHtml(content); paths.hassio_publicPath
)
);
fs.outputFileSync( gulp.task(
path.resolve(paths.gallery_output_root, "index.html"), "gen-pages-hassio-prod",
minified genPagesProdTask(
); HASSIO_PAGE_ENTRIES,
}); paths.hassio_dir,
paths.hassio_output_root,
gulp.task("gen-index-hassio-dev", async () => {
writeHassioEntrypoint(
`${paths.hassio_publicPath}/frontend_latest/entrypoint.js`,
`${paths.hassio_publicPath}/frontend_es5/entrypoint.js`
);
});
gulp.task("gen-index-hassio-prod", async () => {
const latestManifest = require(path.resolve(
paths.hassio_output_latest, paths.hassio_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.hassio_output_es5, paths.hassio_output_es5,
"manifest.json" "src"
)); )
writeHassioEntrypoint( );
latestManifest["entrypoint.js"],
es5Manifest["entrypoint.js"]
);
});
function writeHassioEntrypoint(latestEntrypoint, es5Entrypoint) {
fs.mkdirSync(paths.hassio_output_root, { recursive: true });
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
fs.writeFileSync(
path.resolve(paths.hassio_output_root, "entrypoint.js"),
`
function loadES5() {
var el = document.createElement('script');
el.src = '${es5Entrypoint}';
document.body.appendChild(el);
}
if (/.*Version\\/(?:11|12)(?:\\.\\d+)*.*Safari\\//.test(navigator.userAgent)) {
loadES5();
} else {
try {
new Function("import('${latestEntrypoint}')")();
} catch (err) {
loadES5();
}
}
`,
{ encoding: "utf-8" }
);
}

View File

@ -3,7 +3,7 @@ const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const { marked } = require("marked"); const { marked } = require("marked");
const glob = require("glob"); const { glob } = require("glob");
const yaml = require("js-yaml"); const yaml = require("js-yaml");
const env = require("../env.cjs"); const env = require("../env.cjs");
@ -159,7 +159,7 @@ gulp.task(
"gather-gallery-pages" "gather-gallery-pages"
), ),
"copy-static-gallery", "copy-static-gallery",
"gen-index-gallery-dev", "gen-pages-gallery-dev",
gulp.parallel( gulp.parallel(
env.useRollup() env.useRollup()
? "rollup-dev-server-gallery" ? "rollup-dev-server-gallery"
@ -193,6 +193,6 @@ gulp.task(
), ),
"copy-static-gallery", "copy-static-gallery",
env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery", env.useRollup() ? "rollup-prod-gallery" : "webpack-prod-gallery",
"gen-index-gallery-prod" "gen-pages-gallery-prod"
) )
); );

View File

@ -17,7 +17,7 @@ gulp.task(
}, },
"clean-hassio", "clean-hassio",
"gen-dummy-icons-json", "gen-dummy-icons-json",
"gen-index-hassio-dev", "gen-pages-hassio-dev",
"build-supervisor-translations", "build-supervisor-translations",
"copy-translations-supervisor", "copy-translations-supervisor",
"build-locale-data", "build-locale-data",
@ -39,7 +39,7 @@ gulp.task(
"build-locale-data", "build-locale-data",
"copy-locale-data-supervisor", "copy-locale-data-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio", env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod", "gen-pages-hassio-prod",
...// Don't compress running tests ...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"]) (env.isTestBuild() ? [] : ["compress-hassio"])
) )

View File

@ -19,6 +19,7 @@ const modules = {
"intl-relativetimeformat": "RelativeTimeFormat", "intl-relativetimeformat": "RelativeTimeFormat",
"intl-datetimeformat": "DateTimeFormat", "intl-datetimeformat": "DateTimeFormat",
"intl-numberformat": "NumberFormat", "intl-numberformat": "NumberFormat",
"intl-displaynames": "DisplayNames",
}; };
gulp.task("create-locale-data", (done) => { gulp.task("create-locale-data", (done) => {

View File

@ -0,0 +1,30 @@
#!/usr/bin/env node
// Script to print Babel plugins that will be used by browserslist environments
import { version as babelVersion } from "@babel/core";
import presetEnv from "@babel/preset-env";
import { babelOptions } from "./bundle.cjs";
const dummyAPI = {
version: babelVersion,
assertVersion: () => {},
caller: (callback) =>
callback({
name: "Dummy Bundler",
supportsStaticESM: true,
supportsDynamicImport: true,
supportsTopLevelAwait: true,
supportsExportNamespaceFrom: true,
}),
targets: () => ({}),
};
for (const browserslistEnv of ["modern", "legacy"]) {
console.log("\nBrowsersList Environment = %s\n", browserslistEnv);
presetEnv.default(dummyAPI, {
...babelOptions({ latestBuild: browserslistEnv === "modern" })
.presets[0][1],
browserslistEnv,
debug: true,
});
}

View File

@ -0,0 +1,24 @@
<meta property="fb:app_id" content="338291289691179" />
<meta property="og:title" content="Home Assistant Cast" />
<meta property="og:site_name" content="Home Assistant Cast" />
<meta property="og:url" content="https://cast.home-assistant.io/" />
<meta property="og:type" content="website" />
<meta
property="og:description"
content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen."
/>
<meta
property="og:image"
content="https://cast.home-assistant.io/images/google-nest-hub.png"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@home_assistant" />
<meta name="twitter:title" content="Home Assistant Cast" />
<meta
name="twitter:description"
content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen."
/>
<meta
name="twitter:image"
content="https://cast.home-assistant.io/images/google-nest-hub.png"
/>

View File

@ -3,7 +3,7 @@
<head> <head>
<title>Home Assistant Cast - FAQ</title> <title>Home Assistant Cast - FAQ</title>
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" /> <link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
<%= renderTemplate('_style_base') %> <%= renderTemplate("../../../src/html/_style_base.html.template") %>
<style> <style>
body { body {
background-color: #e5e5e5; background-color: #e5e5e5;
@ -35,25 +35,14 @@
/> />
</head> </head>
<body> <body>
<%= renderTemplate('_js_base') %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<script> <script>
import("<%= latestLauncherJS %>"); <% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true; window.latestJS = true;
</script> </script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
</script>
<hc-layout subtitle="FAQ"> <hc-layout subtitle="FAQ">
<style> <style>
a { a {

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Home Assistant Cast</title>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
<style>
body {
background-color: #e5e5e5;
}
</style>
<%= renderTemplate("_social_meta.html.template") %>
</head>
<body>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<hc-connect></hc-connect>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-57927901-9', 'auto');
ga('send', 'pageview', location.pathname.includes("auth_callback") === -1 ? location.pathname : "/");
</script>
</body>
</html>

View File

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Home Assistant Cast</title>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/images/ha-cast-icon.png" type="image/png" />
<%= renderTemplate('_style_base') %>
<style>
body {
background-color: #e5e5e5;
}
</style>
<meta property="fb:app_id" content="338291289691179">
<meta property="og:title" content="Home Assistant Cast">
<meta property="og:site_name" content="Home Assistant Cast">
<meta property="og:url" content="https://cast.home-assistant.io/">
<meta property="og:type" content="website">
<meta property="og:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
<meta property="og:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@home_assistant">
<meta name="twitter:title" content="Home Assistant Cast">
<meta name="twitter:description" content="Show Home Assistant on your Chromecast or Google Assistant devices with a screen.">
<meta name="twitter:image" content="https://cast.home-assistant.io/images/google-nest-hub.png">
</head>
<body>
<%= renderTemplate('_js_base') %>
<hc-connect></hc-connect>
<script>
import("<%= latestLauncherJS %>");
window.latestJS = true;
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5LauncherJS %>");
};
<% } else { %>
_ls("<%= es5LauncherJS %>");
<% } %>
}
</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-57927901-9', 'auto');
ga('send', 'pageview', location.pathname.includes("auth_callback") === -1 ? location.pathname : "/");
</script>
</body>
</html>

View File

@ -22,25 +22,14 @@
</script> </script>
</head> </head>
<body> <body>
<%= renderTemplate('_js_base') %> <%= renderTemplate("../../../src/html/_js_base.html.template") %>
<cast-media-player></cast-media-player> <cast-media-player></cast-media-player>
<script> <script>
import("<%= latestMediaJS %>"); <% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true; window.latestJS = true;
</script> </script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5MediaJS %>");
};
<% } else { %>
_ls("<%= es5MediaJS %>");
<% } %>
}
</script>
</body> </body>
</html> </html>

View File

@ -1,8 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script> <script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<script type="module" src="<%= latestReceiverJS %>"></script> <% for (const entry of latestEntryJS) { %>
<%= renderTemplate('_style_base') %> <script type="module" src="<%= entry %>"></script>
<% } %>
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
<style> <style>
body { body {
background-color: white; background-color: white;

View File

@ -0,0 +1,26 @@
<meta property="fb:app_id" content="338291289691179" />
<meta property="og:title" content="Home Assistant Demo" />
<meta property="og:site_name" content="Home Assistant" />
<meta property="og:url" content="https://demo.home-assistant.io/" />
<meta property="og:type" content="website" />
<meta
property="og:description"
content="Open source home automation that puts local control and privacy first."
/>
<meta
property="og:image"
content="https://www.home-assistant.io/images/default-social.png"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@home_assistant" />
<meta name="twitter:title" content="Home Assistant" />
<meta
name="twitter:description"
content="Open source home automation that puts local control and privacy first."
/>
<meta
name="twitter:image"
content="https://www.home-assistant.io/images/default-social.png"
/>

View File

@ -1,9 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <title>Home Assistant Demo</title>
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" /> <%= renderTemplate("../../../src/html/_header.html.template") %>
<link rel="icon" href="/static/icons/favicon.ico" />
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" /> <link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
@ -35,33 +34,7 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no" content="width=device-width, initial-scale=1, shrink-to-fit=no"
/> />
<meta name="theme-color" content="#03a9f4" /> <meta name="theme-color" content="#03a9f4" />
<meta property="fb:app_id" content="338291289691179" /> <%= renderTemplate("_social_meta.html.template") %>
<meta property="og:title" content="Home Assistant Demo" />
<meta property="og:site_name" content="Home Assistant" />
<meta property="og:url" content="https://demo.home-assistant.io/" />
<meta property="og:type" content="website" />
<meta
property="og:description"
content="Open source home automation that puts local control and privacy first."
/>
<meta
property="og:image"
content="https://www.home-assistant.io/images/default-social.png"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@home_assistant" />
<meta name="twitter:title" content="Home Assistant" />
<meta
name="twitter:description"
content="Open source home automation that puts local control and privacy first."
/>
<meta
name="twitter:image"
content="https://www.home-assistant.io/images/default-social.png"
/>
<title>Home Assistant Demo</title>
<style> <style>
html { html {
background-color: var(--primary-background-color, #fafafa); background-color: var(--primary-background-color, #fafafa);
@ -107,29 +80,22 @@
</svg> </svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div> <div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
</div> </div>
<ha-demo></ha-demo> <ha-demo></ha-demo>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate('_js_base') %> <%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<%= renderTemplate('_preload_roboto') %>
<script> <script>
import("<%= latestDemoJS %>"); if (!window.globalThis) {
window.latestJS = true; window.globalThis = window;
</script> }
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
<script> if (!isS11_12) {
if (!window.latestJS) { <% for (const entry of latestEntryJS) { %>
<% if (useRollup) { %> import("<%= entry %>");
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>");
};
<% } else { %>
_ls("<%= es5DemoJS %>");
<% } %> <% } %>
window.latestJS = true;
} }
</script> </script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script> <script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]]; var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) { (function (d, t) {

View File

@ -8,8 +8,9 @@
/> />
<meta name="theme-color" content="#2157BC" /> <meta name="theme-color" content="#2157BC" />
<title>Home Assistant Design</title> <title>Home Assistant Design</title>
<% for (const entry of latestEntryJS) { %>
<script type="module" src="<%= latestGalleryJS %>"></script> <script type="module" src="<%= entry %>"></script>
<% } %>
<style> <style>
body { body {
font-family: Roboto, Noto, sans-serif; font-family: Roboto, Noto, sans-serif;

View File

@ -336,7 +336,7 @@ const SCHEMAS: {
["and", "another_one"], ["and", "another_one"],
["option", "1000"], ["option", "1000"],
], ],
name: "select many otions", name: "select many options",
default: "default", default: "default",
}, },
], ],
@ -364,7 +364,7 @@ const SCHEMAS: {
and: "another_one", and: "another_one",
option: "1000", option: "1000",
}, },
name: "multi many otions", name: "multi many options",
default: ["default"], default: ["default"],
}, },
], ],

View File

@ -216,7 +216,7 @@ export class HassioAddonStore extends LitElement {
}); });
} }
private async _filterChanged(e) { private _filterChanged(e) {
this._filter = e.detail.value; this._filter = e.detail.value;
} }

View File

@ -29,7 +29,6 @@ import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate"; import { navigate } from "../../../../src/common/navigate";
import "../../../../src/components/buttons/ha-call-api-button";
import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert"; import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
@ -47,6 +46,7 @@ import {
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
HassioAddonSetSecurityParams, HassioAddonSetSecurityParams,
installHassioAddon, installHassioAddon,
rebuildLocalAddon,
restartHassioAddon, restartHassioAddon,
setHassioAddonOption, setHassioAddonOption,
setHassioAddonSecurity, setHassioAddonSecurity,
@ -640,13 +640,12 @@ class HassioAddonInfo extends LitElement {
</ha-progress-button> </ha-progress-button>
${this.addon.build ${this.addon.build
? html` ? html`
<ha-call-api-button <ha-progress-button
class="warning" class="warning"
.hass=${this.hass} @click=${this._rebuildClicked}
.path="hassio/addons/${this.addon.slug}/rebuild"
> >
${this.supervisor.localize("addon.dashboard.rebuild")} ${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-call-api-button> </ha-progress-button>
` `
: ""}` : ""}`
: ""} : ""}
@ -966,6 +965,21 @@ class HassioAddonInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _rebuildClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await rebuildLocalAddon(this.hass, this.addon.slug);
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _startClicked(ev: CustomEvent): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
@ -1124,10 +1138,6 @@ class HassioAddonInfo extends LitElement {
ha-svg-icon.stopped { ha-svg-icon.stopped {
color: var(--error-color); color: var(--error-color);
} }
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
protection-enable mwc-button { protection-enable mwc-button {
--mdc-theme-primary: white; --mdc-theme-primary: white;
} }

View File

@ -316,7 +316,7 @@ export class DialogHassioNetwork
> >
<div class="radio-row"> <div class="radio-row">
<ha-formfield <ha-formfield
.label=${this.supervisor.localize("dialog.network.dhcp")} .label=${this.supervisor.localize("dialog.network.auto")}
> >
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}

View File

@ -0,0 +1,22 @@
(function () {
function loadES5(src) {
var el = document.createElement("script");
el.src = src;
document.body.appendChild(el);
}
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
} else {
try {
<% for (const entry of latestEntryJS) { %>
new Function("import('<%= entry %>')")();
<% } %>
} catch (err) {
<% for (const entry of es5EntryJS) { %>
loadES5("<%= entry %>");
<% } %>
}
}
})();

View File

@ -44,10 +44,6 @@ export const hassioStyle = css`
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
} }
} }
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error { .error {
color: var(--error-color); color: var(--error-color);
margin-top: 16px; margin-top: 16px;

View File

@ -26,20 +26,21 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.2", "@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.4.2", "@codemirror/autocomplete": "6.5.1",
"@codemirror/commands": "6.2.2", "@codemirror/commands": "6.2.3",
"@codemirror/language": "6.6.0", "@codemirror/language": "6.6.0",
"@codemirror/legacy-modes": "6.3.2", "@codemirror/legacy-modes": "6.3.2",
"@codemirror/search": "6.3.0", "@codemirror/search": "6.3.0",
"@codemirror/state": "6.2.0", "@codemirror/state": "6.2.0",
"@codemirror/view": "6.9.3", "@codemirror/view": "6.9.5",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.5.1", "@formatjs/intl-datetimeformat": "6.7.0",
"@formatjs/intl-displaynames": "6.3.1",
"@formatjs/intl-getcanonicallocales": "2.1.0", "@formatjs/intl-getcanonicallocales": "2.1.0",
"@formatjs/intl-locale": "3.1.1", "@formatjs/intl-locale": "3.2.1",
"@formatjs/intl-numberformat": "8.3.5", "@formatjs/intl-numberformat": "8.4.1",
"@formatjs/intl-pluralrules": "5.1.10", "@formatjs/intl-pluralrules": "5.2.1",
"@formatjs/intl-relativetimeformat": "11.1.10", "@formatjs/intl-relativetimeformat": "11.2.1",
"@fullcalendar/core": "6.1.5", "@fullcalendar/core": "6.1.5",
"@fullcalendar/daygrid": "6.1.5", "@fullcalendar/daygrid": "6.1.5",
"@fullcalendar/interaction": "6.1.5", "@fullcalendar/interaction": "6.1.5",
@ -90,8 +91,8 @@
"@polymer/paper-toast": "3.0.1", "@polymer/paper-toast": "3.0.1",
"@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": "23.3.10", "@vaadin/combo-box": "23.3.11",
"@vaadin/vaadin-themable-mixin": "23.3.10", "@vaadin/vaadin-themable-mixin": "23.3.11",
"@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",
@ -101,18 +102,18 @@
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "3.3.2", "chart.js": "3.3.2",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.30.0", "core-js": "3.30.1",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
"date-fns": "2.29.3", "date-fns": "2.29.3",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"fuse.js": "6.6.2", "fuse.js": "6.6.2",
"google-timezones-json": "1.0.2", "google-timezones-json": "1.1.0",
"hls.js": "1.3.5", "hls.js": "1.3.5",
"home-assistant-js-websocket": "8.0.1", "home-assistant-js-websocket": "8.0.1",
"idb-keyval": "6.2.0", "idb-keyval": "6.2.0",
"intl-messageformat": "10.3.3", "intl-messageformat": "10.3.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.3", "leaflet": "1.9.3",
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
@ -149,15 +150,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.4", "@babel/core": "7.21.4",
"@babel/plugin-external-helpers": "7.18.6",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-decorators": "7.21.0", "@babel/plugin-proposal-decorators": "7.21.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-object-rest-spread": "7.20.7",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-syntax-import-meta": "7.10.4",
"@babel/plugin-syntax-top-level-await": "7.14.5",
"@babel/preset-env": "7.21.4", "@babel/preset-env": "7.21.4",
"@babel/preset-typescript": "7.21.4", "@babel/preset-typescript": "7.21.4",
"@koa/cors": "4.0.0", "@koa/cors": "4.0.0",
@ -166,7 +159,7 @@
"@octokit/rest": "19.0.7", "@octokit/rest": "19.0.7",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.3", "@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "24.0.1", "@rollup/plugin-commonjs": "24.1.0",
"@rollup/plugin-json": "6.0.0", "@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.0.2", "@rollup/plugin-node-resolve": "15.0.2",
"@rollup/plugin-replace": "5.0.2", "@rollup/plugin-replace": "5.0.2",
@ -185,8 +178,8 @@
"@types/sortablejs": "1.15.1", "@types/sortablejs": "1.15.1",
"@types/tar": "6.1.4", "@types/tar": "6.1.4",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.57.1", "@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.57.1", "@typescript-eslint/parser": "5.59.0",
"@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.2", "babel-loader": "9.1.2",
@ -200,23 +193,23 @@
"eslint-import-resolver-webpack": "0.13.2", "eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-lit": "1.8.2", "eslint-plugin-lit": "1.8.3",
"eslint-plugin-lit-a11y": "2.4.1", "eslint-plugin-lit-a11y": "2.4.1",
"eslint-plugin-unused-imports": "2.0.0", "eslint-plugin-unused-imports": "2.0.0",
"eslint-plugin-wc": "1.4.0", "eslint-plugin-wc": "1.4.0",
"esprima": "4.0.1", "esprima": "4.0.1",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",
"glob": "9.3.4", "glob": "10.2.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-flatmap": "1.0.2", "gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8", "gulp-json-transform": "0.4.8",
"gulp-merge-json": "2.1.2", "gulp-merge-json": "2.1.2",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1", "gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.1.0", "html-minifier-terser": "7.2.0",
"husky": "8.0.3", "husky": "8.0.3",
"instant-mocha": "1.5.0", "instant-mocha": "1.5.1",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "13.2.1", "lint-staged": "13.2.1",
"lit-analyzer": "1.2.1", "lit-analyzer": "1.2.1",
@ -234,7 +227,7 @@
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.0", "rollup-plugin-visualizer": "5.9.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "15.0.3", "sinon": "15.0.4",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.1", "systemjs": "6.14.1",
"tar": "6.1.13", "tar": "6.1.13",
@ -245,7 +238,7 @@
"vinyl-source-stream": "2.0.0", "vinyl-source-stream": "2.0.0",
"webpack": "=5.72.1", "webpack": "=5.72.1",
"webpack-cli": "5.0.1", "webpack-cli": "5.0.1",
"webpack-dev-server": "4.13.2", "webpack-dev-server": "4.13.3",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpackbar": "5.0.2", "webpackbar": "5.0.2",
"workbox-build": "6.5.4" "workbox-build": "6.5.4"

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20230411.1" version = "20230426.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

@ -9,6 +9,7 @@ import {
PropertyValues, PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-checkbox"; import "../components/ha-checkbox";
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data"; import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
@ -20,13 +21,12 @@ import {
DataEntryFlowStep, DataEntryFlowStep,
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import "./ha-password-manager-polyfill"; import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@customElement("ha-auth-flow") @customElement("ha-auth-flow")
export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { export class HaAuthFlow extends LitElement {
@property({ attribute: false }) public authProvider?: AuthProvider; @property({ attribute: false }) public authProvider?: AuthProvider;
@property() public clientId?: string; @property() public clientId?: string;
@ -35,6 +35,8 @@ export class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string; @property() public oauth2State?: string;
@property() public localize!: LocalizeFunc;
@state() private _state: State = "loading"; @state() private _state: State = "loading";
@state() private _stepData?: Record<string, any>; @state() private _stepData?: Record<string, any>;

View File

@ -82,12 +82,13 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
.redirectUri=${this.redirectUri} .redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State} .oauth2State=${this.oauth2State}
.authProvider=${this._authProvider} .authProvider=${this._authProvider}
.localize=${this.localize}
></ha-auth-flow> ></ha-auth-flow>
${inactiveProviders.length > 0 ${inactiveProviders.length > 0
? html` ? html`
<ha-pick-auth-provider <ha-pick-auth-provider
.resources=${this.resources} .localize=${this.localize}
.clientId=${this.clientId} .clientId=${this.clientId}
.authProviders=${inactiveProviders} .authProviders=${inactiveProviders}
@pick-auth-provider=${this._handleAuthProviderPick} @pick-auth-provider=${this._handleAuthProviderPick}

View File

@ -2,10 +2,10 @@ import "@material/mwc-list";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-icon-next"; import "../components/ha-icon-next";
import "../components/ha-list-item"; import "../components/ha-list-item";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@ -14,9 +14,11 @@ declare global {
} }
@customElement("ha-pick-auth-provider") @customElement("ha-pick-auth-provider")
export class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) { export class HaPickAuthProvider extends LitElement {
@property() public authProviders: AuthProvider[] = []; @property() public authProviders: AuthProvider[] = [];
@property() public localize!: LocalizeFunc;
protected render() { protected render() {
return html` return html`
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p> <p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>

View File

@ -50,6 +50,7 @@ import {
mdiRobotVacuum, mdiRobotVacuum,
mdiScriptText, mdiScriptText,
mdiSineWave, mdiSineWave,
mdiSpeakerMessage,
mdiSpeedometer, mdiSpeedometer,
mdiSunWireless, mdiSunWireless,
mdiThermometer, mdiThermometer,
@ -75,8 +76,8 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */ /** Icons for each domain */
export const FIXED_DOMAIN_ICONS = { export const FIXED_DOMAIN_ICONS = {
alert: mdiAlert,
air_quality: mdiAirFilter, air_quality: mdiAirFilter,
alert: mdiAlert,
calendar: mdiCalendar, calendar: mdiCalendar,
climate: mdiThermostat, climate: mdiThermostat,
configurator: mdiCog, configurator: mdiCog,
@ -106,10 +107,12 @@ export const FIXED_DOMAIN_ICONS = {
script: mdiScriptText, script: mdiScriptText,
select: mdiFormatListBulleted, select: mdiFormatListBulleted,
sensor: mdiEye, sensor: mdiEye,
siren: mdiBullhorn,
simple_alarm: mdiBell, simple_alarm: mdiBell,
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
text: mdiFormTextbox, text: mdiFormTextbox,
timer: mdiTimerOutline, timer: mdiTimerOutline,
tts: mdiSpeakerMessage,
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,

View File

@ -1,7 +0,0 @@
export const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
export const SpeechGrammarList =
window.SpeechGrammarList || window.webkitSpeechGrammarList;
export const SpeechRecognitionEvent =
// @ts-expect-error
window.SpeechRecognitionEvent || window.webkitSpeechRecognitionEvent;

View File

@ -193,11 +193,9 @@ export const computeStateDisplayFromEntityAttributes = (
); );
} }
// state of button is a timestamp // state is a timestamp
if ( if (
domain === "button" || ["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
domain === "input_button" ||
domain === "scene" ||
(domain === "sensor" && attributes.device_class === "timestamp") (domain === "sensor" && attributes.device_class === "timestamp")
) { ) {
try { try {

View File

@ -0,0 +1,22 @@
import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation";
export const formatLanguageCode = (
languageCode: string,
locale: FrontendLocaleData
) => {
try {
return formatLanguageCodeMem(locale)?.of(languageCode) ?? languageCode;
} catch {
return languageCode;
}
};
const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) =>
Intl && "DisplayNames" in Intl
? new Intl.DisplayNames(locale.language, {
type: "language",
fallback: "code",
})
: undefined
);

View File

@ -2,6 +2,7 @@ import { shouldPolyfill as shouldPolyfillLocale } from "@formatjs/intl-locale/li
import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill"; import { shouldPolyfill as shouldPolyfillDateTime } from "@formatjs/intl-datetimeformat/lib/should-polyfill";
import { shouldPolyfill as shouldPolyfillDisplayName } from "@formatjs/intl-displaynames/lib/should-polyfill";
import IntlMessageFormat from "intl-messageformat"; import IntlMessageFormat from "intl-messageformat";
import { Resources, TranslationDict } from "../../types"; import { Resources, TranslationDict } from "../../types";
import { getLocalLanguage } from "../../util/common-translation"; import { getLocalLanguage } from "../../util/common-translation";
@ -83,6 +84,10 @@ if (__BUILD__ === "latest") {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill")); polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz")); polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
} }
if (shouldPolyfillDisplayName(locale)) {
polyfills.push(import("@formatjs/intl-displaynames/polyfill"));
polyfills.push(import("@formatjs/intl-displaynames/locale-data/en"));
}
} }
export const polyfillsLoaded = export const polyfillsLoaded =
@ -216,6 +221,17 @@ export const loadPolyfillLocales = async (language: string) => {
// @ts-ignore // @ts-ignore
Intl.DateTimeFormat.__addLocaleData(await result.json()); Intl.DateTimeFormat.__addLocaleData(await result.json());
} }
if (
Intl.DisplayNames &&
// @ts-ignore
typeof Intl.DisplayNames.__addLocaleData === "function"
) {
const result = await fetch(
`/static/locale-data/intl-displaynames/${language}.json`
);
// @ts-ignore
Intl.DisplayNames.__addLocaleData(await result.json());
}
} catch (e) { } catch (e) {
// Ignore // Ignore
} }

View File

@ -1,77 +0,0 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "./ha-progress-button";
@customElement("ha-call-api-button")
class HaCallApiButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public method: "POST" | "GET" | "PUT" | "DELETE" = "POST";
@property() public data = {};
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public path?: string;
@query("ha-progress-button", true) private _progressButton;
render() {
return html`
<ha-progress-button
.progress=${this.progress}
@click=${this._buttonTapped}
?disabled=${this.disabled}
><slot></slot
></ha-progress-button>
`;
}
async _buttonTapped() {
this.progress = true;
const eventData: {
method: string;
path: string;
data: any;
success?: boolean;
response?: any;
} = {
method: this.method,
path: this.path!,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path!, this.data);
this.progress = false;
this._progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err: any) {
this.progress = false;
this._progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData as any);
}
static get styles(): CSSResultGroup {
return css`
:host([disabled]) {
pointer-events: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;
}
}

View File

@ -73,7 +73,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
main?: boolean; main?: boolean;
title: TemplateResult | string; title: TemplateResult | string;
label?: TemplateResult | string; label?: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button" | "overflow-menu"; type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
template?: (data: any, row: T) => TemplateResult | string | typeof nothing; template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
@ -359,10 +359,10 @@ export class HaDataTable extends LitElement {
return nothing; return nothing;
} }
if (row.append) { if (row.append) {
return html` <div class="mdc-data-table__row">${row.content}</div> `; return html`<div class="mdc-data-table__row">${row.content}</div>`;
} }
if (row.empty) { if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `; return html`<div class="mdc-data-table__row"></div>`;
} }
return html` return html`
<div <div
@ -406,6 +406,7 @@ export class HaDataTable extends LitElement {
<div <div
role=${column.main ? "rowheader" : "cell"} role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex",
"mdc-data-table__cell--numeric": column.type === "numeric", "mdc-data-table__cell--numeric": column.type === "numeric",
"mdc-data-table__cell--icon": column.type === "icon", "mdc-data-table__cell--icon": column.type === "icon",
"mdc-data-table__cell--icon-button": "mdc-data-table__cell--icon-button":
@ -663,6 +664,10 @@ export class HaDataTable extends LitElement {
box-sizing: border-box; box-sizing: border-box;
} }
.mdc-data-table__cell.mdc-data-table__cell--flex {
display: flex;
}
.mdc-data-table__cell.mdc-data-table__cell--icon { .mdc-data-table__cell.mdc-data-table__cell--icon {
overflow: initial; overflow: initial;
} }

View File

@ -0,0 +1,129 @@
import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-area-picker";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-aliases-editor")
class AliasesEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public aliases!: string[];
protected render() {
if (!this.aliases) {
return nothing;
}
return html`
${this.aliases.map(
(alias, index) => html`
<div class="layout horizontal center-center row">
<ha-textfield
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize("ui.dialogs.aliases.input_label", {
number: index + 1,
})}
.value=${alias}
?data-last=${index === this.aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
number: index + 1,
})}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
></ha-icon-button>
</div>
`
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
`;
}
private async _addAlias() {
this.aliases = [...this.aliases, ""];
this._fireChanged(this.aliases);
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
| HaTextField
| undefined;
field?.focus();
}
private async _editAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this.aliases];
aliases[index] = (ev.target as any).value;
this._fireChanged(aliases);
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this.aliases];
aliases.splice(index, 1);
this._fireChanged(aliases);
}
private _fireChanged(value) {
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.row {
margin-bottom: 8px;
}
ha-textfield {
display: block;
}
ha-icon-button {
display: block;
}
mwc-button {
margin-left: 8px;
}
#alias_input {
margin-top: 8px;
}
.alias {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
--mdc-icon-button-size: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-aliases-editor": AliasesEditor;
}
}

View File

@ -0,0 +1,109 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValueMap,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import { AssistPipeline, listAssistPipelines } from "../data/assist_pipeline";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const PREFERRED = "__PREFERRED_PIPELINE_OPTION__";
@customElement("ha-assist-pipeline-picker")
export class HaAssistPipelinePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _pipelines?: AssistPipeline[];
@state() _preferredPipeline: string | null = null;
protected render() {
if (!this._pipelines) {
return nothing;
}
const value = this.value ?? PREFERRED;
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.pipeline-picker.pipeline")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item .value=${PREFERRED}>
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
})}
</ha-list-item>
${this._pipelines.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline.id}>
${pipeline.name}
(${formatLanguageCode(pipeline.language, this.hass.locale)})
</ha-list-item>`
)}
</ha-select>
`;
}
protected firstUpdated(
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.firstUpdated(changedProperties);
listAssistPipelines(this.hass).then((pipelines) => {
this._pipelines = pipelines.pipelines;
this._preferredPipeline = pipelines.preferred_pipeline;
});
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === PREFERRED)
) {
return;
}
this.value = target.value === PREFERRED ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-assist-pipeline-picker": HaAssistPipelinePicker;
}
}

View File

@ -13,6 +13,9 @@ export class HaButton extends Button {
margin-inline-end: 8px; margin-inline-end: 8px;
direction: var(--direction); direction: var(--direction);
} }
.mdc-button {
height: var(--button-height, 36px);
}
`, `,
]; ];
} }

View File

@ -14,8 +14,9 @@ import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield"; import type { HaTextField } from "./ha-textfield";
registerStyles( registerStyles(

View File

@ -0,0 +1,159 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import { Agent, listAgents } from "../data/conversation";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@customElement("ha-conversation-agent-picker")
export class HaConversationAgentPicker extends LitElement {
@property() public value?: string;
@property() public language?: string;
@property() public label?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _agents?: Agent[];
protected render() {
if (!this._agents) {
return nothing;
}
const value =
this.value ??
(this.required &&
(!this.language ||
this._agents
.find((agent) => agent.id === "homeassistant")
?.supported_languages.includes(this.language))
? "homeassistant"
: NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.coversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateAgents();
} else if (changedProperties.has("language")) {
this._debouncedUpdateAgents();
}
}
private _debouncedUpdateAgents = debounce(() => this._updateAgents(), 500);
private async _updateAgents() {
const { agents } = await listAgents(
this.hass,
this.language,
this.hass.config.country || undefined
);
this._agents = agents;
if (!this.value) {
return;
}
const selectedAgent = agents.find((agent) => agent.id === this.value);
fireEvent(this, "supported-languages-changed", {
value: selectedAgent?.supported_languages,
});
if (
!selectedAgent ||
(selectedAgent.supported_languages !== "*" &&
selectedAgent.supported_languages.length === 0)
) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._agents!.find((agent) => agent.id === this.value)
?.supported_languages,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-conversation-agent-picker": HaConversationAgentPicker;
}
interface HASSDomEvents {
"supported-languages-changed": { value: "*" | string[] | undefined };
}
}

View File

@ -7,7 +7,7 @@ import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-icon-button"; import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"]; const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = ( export const createCloseHeading = (
hass: HomeAssistant, hass: HomeAssistant,
@ -92,7 +92,7 @@ export class HaDialog extends DialogBase {
padding: 24px 24px 0 24px; padding: 24px 24px 0 24px;
} }
.mdc-dialog__actions { .mdc-dialog__actions {
padding: 0 24px 24px 24px; padding: 12px 24px 12px 24px;
} }
.mdc-dialog__title::before { .mdc-dialog__title::before {
display: block; display: block;

View File

@ -1,9 +1,3 @@
import {
DIRECTION_LEFT,
DIRECTION_RIGHT,
Manager,
Swipe,
} from "@egjs/hammerjs";
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base"; import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
import { styles } from "@material/mwc-drawer/mwc-drawer.css"; import { styles } from "@material/mwc-drawer/mwc-drawer.css";
import { css, PropertyValues } from "lit"; import { css, PropertyValues } from "lit";
@ -40,35 +34,51 @@ export class HaDrawer extends DrawerBase {
this.mdcRoot.dir = this.direction; this.mdcRoot.dir = this.direction;
} }
if (changedProps.has("open") && this.open && this.type === "modal") { if (changedProps.has("open") && this.open && this.type === "modal") {
this._mc = new Manager(document, { this._setupSwipe();
touchAction: "pan-y",
});
this._mc.add(
new Swipe({
direction:
this.direction === "rtl" ? DIRECTION_RIGHT : DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
} else if (this._mc) { } else if (this._mc) {
this._mc.destroy(); this._mc.destroy();
this._mc = undefined; this._mc = undefined;
} }
} }
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
touchAction: "pan-y",
});
this._mc.add(
new hammer.Swipe({
direction:
this.direction === "rtl"
? hammer.DIRECTION_RIGHT
: hammer.DIRECTION_LEFT,
})
);
this._mc.on("swipeleft swiperight", () => {
fireEvent(this, "hass-toggle-menu", { open: false });
});
}
static override styles = [ static override styles = [
styles, styles,
css` css`
.mdc-drawer { .mdc-drawer {
position: fixed;
top: 0; top: 0;
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
} }
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open { .mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200; z-index: 200;
} }
.mdc-drawer-app-content { .mdc-drawer-app-content {
transform: translateZ(0); overflow: unset;
flex: none;
padding-left: var(--mdc-drawer-width);
padding-inline-start: var(--mdc-drawer-width);
padding-inline-end: initial;
direction: var(--direction);
width: 100%;
box-sizing: border-box;
} }
`, `,
]; ];

View File

@ -71,6 +71,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
display: block; display: block;
--expansion-panel-content-padding: 0; --expansion-panel-content-padding: 0;
border-radius: 6px; border-radius: 6px;
--ha-card-border-radius: 6px;
} }
ha-svg-icon, ha-svg-icon,
ha-icon { ha-icon {

View File

@ -33,6 +33,11 @@ export class HaFormGrid extends LitElement implements HaFormElement {
@property() public computeHelper?: (schema: HaFormSchema) => string; @property() public computeHelper?: (schema: HaFormSchema) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
}
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("schema")) { if (changedProps.has("schema")) {

View File

@ -4,6 +4,7 @@ import {
html, html,
LitElement, LitElement,
PropertyValues, PropertyValues,
ReactiveElement,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@ -56,13 +57,18 @@ export class HaForm extends LitElement implements HaFormElement {
@property() public localizeValue?: (key: string) => string; @property() public localizeValue?: (key: string) => string;
public focus() { public async focus() {
const root = this.shadowRoot?.querySelector(".root"); await this.updateComplete;
const root = this.renderRoot.querySelector(".root");
if (!root) { if (!root) {
return; return;
} }
for (const child of root.children) { for (const child of root.children) {
if (child.tagName !== "HA-ALERT") { if (child.tagName !== "HA-ALERT") {
if (child instanceof ReactiveElement) {
// eslint-disable-next-line no-await-in-loop
await child.updateComplete;
}
(child as HTMLElement).focus(); (child as HTMLElement).focus();
break; break;
} }

View File

@ -211,6 +211,7 @@ export class Gauge extends LitElement {
font-size: 50px; font-size: 50px;
fill: var(--primary-text-color); fill: var(--primary-text-color);
text-anchor: middle; text-anchor: middle;
direction: ltr;
} }
`; `;
} }

View File

@ -0,0 +1,162 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
@customElement("ha-language-picker")
export class HaLanguagePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public languages?: string[];
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public nativeName = false;
@property({ type: Boolean }) public noSort = false;
@state() _defaultLanguages: string[] = [];
@query("ha-select") private _select!: HaSelect;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._computeDefaultLanguageOptions();
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("languages") || changedProperties.has("value")) {
this._select.layoutOptions();
if (this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
}
if (!this.value) {
return;
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
);
const selectedItem = languageOptions.find(
(option) => option.value === this.value
);
if (!selectedItem) {
this.value = undefined;
}
}
}
private _getLanguagesOptions = memoizeOne(
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = this.hass.translationMetadata.translations;
options = languages.map((lang) => ({
value: lang,
label: translations[lang]?.nativeName ?? lang,
}));
} else {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!this.noSort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
}
return options;
}
);
private _computeDefaultLanguageOptions() {
if (!this.hass.translationMetadata?.translations) {
return;
}
this._defaultLanguages = Object.keys(
this.hass.translationMetadata.translations
);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
);
const value =
this.value ?? (this.required ? languageOptions[0]?.value : this.value);
return html`
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.language-picker.language")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass.localize(
"ui.components.language-picker.no_languages"
)}</ha-list-item
>`
: languageOptions.map(
(option) => html`
<ha-list-item .value=${option.value}
>${option.label}</ha-list-item
>
`
)}
</ha-select>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (!this.hass || target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-language-picker": HaLanguagePicker;
}
}

View File

@ -30,6 +30,9 @@ export class HaListItem extends ListItemBase {
margin-inline-end: 0px !important; margin-inline-end: 0px !important;
direction: var(--direction); direction: var(--direction);
} }
.mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display);
}
:host([multiline-secondary]) { :host([multiline-secondary]) {
height: auto; height: auto;
} }
@ -54,6 +57,9 @@ export class HaListItem extends ListItemBase {
.mdc-deprecated-list-item__primary-text::before { .mdc-deprecated-list-item__primary-text::before {
display: none; display: none;
} }
:host([disabled]) {
color: var(--disabled-text-color);
}
`, `,
]; ];
} }

View File

@ -0,0 +1,45 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { AssistPipelineSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-assist-pipeline-picker";
@customElement("ha-selector-assist_pipeline")
export class HaAssistPipelineSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AssistPipelineSelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`<ha-assist-pipeline-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-assist-pipeline-picker>`;
}
static styles = css`
ha-conversation-agent-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-assist_pipeline": HaAssistPipelineSelector;
}
}

View File

@ -0,0 +1,51 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ConversationAgentSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-conversation-agent-picker";
@customElement("ha-selector-conversation_agent")
export class HaConversationAgentSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ConversationAgentSelector;
@property() public value?: any;
@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?: {
language?: string;
};
protected render() {
return html`<ha-conversation-agent-picker
.hass=${this.hass}
.value=${this.value}
.language=${this.selector.conversation_agent?.language ||
this.context?.language}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-conversation-agent-picker>`;
}
static styles = css`
ha-conversation-agent-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-conversation_agent": HaConversationAgentSelector;
}
}

View File

@ -0,0 +1,50 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { LanguageSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-language-picker";
@customElement("ha-selector-language")
export class HaLanguageSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: LanguageSelector;
@property() public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
return html`
<ha-language-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.languages=${this.selector.language?.languages}
.nativeName=${Boolean(this.selector?.language?.native_name)}
.noSort=${Boolean(this.selector?.language?.no_sort)}
.disabled=${this.disabled}
.required=${this.required}
></ha-language-picker>
`;
}
static styles = css`
ha-language-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-language": HaLanguageSelector;
}
}

View File

@ -135,7 +135,7 @@ export class HaSelectSelector extends LitElement {
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required && !value.length} .required=${this.required && !value.length}
.value=${this._filter} .value=${""}
.items=${optionItems} .items=${optionItems}
.allowCustomValue=${this.selector.select.custom_value ?? false} .allowCustomValue=${this.selector.select.custom_value ?? false}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@ -213,7 +213,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail?.value || ev.target.value; const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined) { if (this.disabled || value === undefined || value === this.value) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {

View File

@ -0,0 +1,50 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { STTSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-stt-picker";
@customElement("ha-selector-stt")
export class HaSTTSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: STTSelector;
@property() public value?: any;
@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?: {
language?: string;
};
protected render() {
return html`<ha-stt-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.language=${this.selector.stt?.language || this.context?.language}
.disabled=${this.disabled}
.required=${this.required}
></ha-stt-picker>`;
}
static styles = css`
ha-stt-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-stt": HaSTTSelector;
}
}

View File

@ -30,6 +30,13 @@ export class HaTextSelector extends LitElement {
@state() private _unmaskedPassword = false; @state() private _unmaskedPassword = false;
public async focus() {
await this.updateComplete;
(
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
)?.focus();
}
protected render() { protected render() {
if (this.selector.text?.multiline) { if (this.selector.text?.multiline) {
return html`<ha-textarea return html`<ha-textarea

View File

@ -0,0 +1,52 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { TTSVoiceSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-tts-voice-picker";
@customElement("ha-selector-tts_voice")
export class HaTTSVoiceSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TTSVoiceSelector;
@property() public value?: any;
@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?: {
language?: string;
engineId?: string;
};
protected render() {
return html`<ha-tts-voice-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.language=${this.selector.tts_voice?.language || this.context?.language}
.engineId=${this.selector.tts_voice?.engineId || this.context?.engineId}
.disabled=${this.disabled}
.required=${this.required}
></ha-tts-voice-picker>`;
}
static styles = css`
ha-tts-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-tts-voice": HaTTSVoiceSelector;
}
}

View File

@ -0,0 +1,50 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { TTSSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-tts-picker";
@customElement("ha-selector-tts")
export class HaTTSSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TTSSelector;
@property() public value?: any;
@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?: {
language?: string;
};
protected render() {
return html`<ha-tts-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.language=${this.selector.tts?.language || this.context?.language}
.disabled=${this.disabled}
.required=${this.required}
></ha-tts-picker>`;
}
static styles = css`
ha-tts-picker {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-tts": HaTTSSelector;
}
}

View File

@ -6,7 +6,7 @@ import { HomeAssistant } from "../../types";
import "../../panels/lovelace/components/hui-action-editor"; import "../../panels/lovelace/components/hui-action-editor";
import { ActionConfig } from "../../data/lovelace"; import { ActionConfig } from "../../data/lovelace";
@customElement("ha-selector-ui-action") @customElement("ha-selector-ui_action")
export class HaSelectorUiAction extends LitElement { export class HaSelectorUiAction extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -24,7 +24,7 @@ export class HaSelectorUiAction extends LitElement {
.label=${this.label} .label=${this.label}
.hass=${this.hass} .hass=${this.hass}
.config=${this.value} .config=${this.value}
.actions=${this.selector["ui-action"]?.actions} .actions=${this.selector.ui_action?.actions}
.tooltipText=${this.helper} .tooltipText=${this.helper}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></hui-action-editor> ></hui-action-editor>

View File

@ -6,7 +6,7 @@ import { UiColorSelector } from "../../data/selector";
import "../../panels/lovelace/components/hui-color-picker"; import "../../panels/lovelace/components/hui-color-picker";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@customElement("ha-selector-ui-color") @customElement("ha-selector-ui_color")
export class HaSelectorUiColor extends LitElement { export class HaSelectorUiColor extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;

View File

@ -14,9 +14,11 @@ const LOAD_ELEMENTS = {
addon: () => import("./ha-selector-addon"), addon: () => import("./ha-selector-addon"),
area: () => import("./ha-selector-area"), area: () => import("./ha-selector-area"),
attribute: () => import("./ha-selector-attribute"), attribute: () => import("./ha-selector-attribute"),
assist_pipeline: () => import("./ha-selector-assist-pipeline"),
boolean: () => import("./ha-selector-boolean"), boolean: () => import("./ha-selector-boolean"),
color_rgb: () => import("./ha-selector-color-rgb"), color_rgb: () => import("./ha-selector-color-rgb"),
config_entry: () => import("./ha-selector-config-entry"), config_entry: () => import("./ha-selector-config-entry"),
conversation_agent: () => import("./ha-selector-conversation-agent"),
constant: () => import("./ha-selector-constant"), constant: () => import("./ha-selector-constant"),
date: () => import("./ha-selector-date"), date: () => import("./ha-selector-date"),
datetime: () => import("./ha-selector-datetime"), datetime: () => import("./ha-selector-datetime"),
@ -25,11 +27,13 @@ const LOAD_ELEMENTS = {
entity: () => import("./ha-selector-entity"), entity: () => import("./ha-selector-entity"),
statistic: () => import("./ha-selector-statistic"), statistic: () => import("./ha-selector-statistic"),
file: () => import("./ha-selector-file"), file: () => import("./ha-selector-file"),
language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"), navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"), number: () => import("./ha-selector-number"),
object: () => import("./ha-selector-object"), object: () => import("./ha-selector-object"),
select: () => import("./ha-selector-select"), select: () => import("./ha-selector-select"),
state: () => import("./ha-selector-state"), state: () => import("./ha-selector-state"),
stt: () => import("./ha-selector-stt"),
target: () => import("./ha-selector-target"), target: () => import("./ha-selector-target"),
template: () => import("./ha-selector-template"), template: () => import("./ha-selector-template"),
text: () => import("./ha-selector-text"), text: () => import("./ha-selector-text"),
@ -37,12 +41,16 @@ const LOAD_ELEMENTS = {
icon: () => import("./ha-selector-icon"), icon: () => import("./ha-selector-icon"),
media: () => import("./ha-selector-media"), media: () => import("./ha-selector-media"),
theme: () => import("./ha-selector-theme"), theme: () => import("./ha-selector-theme"),
tts: () => import("./ha-selector-tts"),
tts_voice: () => import("./ha-selector-tts-voice"),
location: () => import("./ha-selector-location"), location: () => import("./ha-selector-location"),
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"),
}; };
const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
@customElement("ha-selector") @customElement("ha-selector")
export class HaSelector extends LitElement { export class HaSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@ -67,12 +75,17 @@ export class HaSelector extends LitElement {
@property() public context?: Record<string, any>; @property() public context?: Record<string, any>;
public focus() { public async focus() {
this.shadowRoot?.getElementById("selector")?.focus(); await this.updateComplete;
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
} }
private get _type() { private get _type() {
return Object.keys(this.selector)[0]; const type = Object.keys(this.selector)[0];
if (LEGACY_UI_SELECTORS.has(type)) {
return type.replace("-", "_");
}
return type;
} }
protected willUpdate(changedProps: PropertyValues) { protected willUpdate(changedProps: PropertyValues) {
@ -88,6 +101,10 @@ export class HaSelector extends LitElement {
if ("device" in selector) { if ("device" in selector) {
return handleLegacyDeviceSelector(selector); return handleLegacyDeviceSelector(selector);
} }
const type = Object.keys(this.selector)[0];
if (LEGACY_UI_SELECTORS.has(type)) {
return { [type.replace("-", "_")]: selector[type] };
}
return selector; return selector;
}); });

View File

@ -52,18 +52,17 @@ export class HaSettingsRow extends LitElement {
white-space: nowrap; white-space: nowrap;
} }
.body > .secondary { .body > .secondary {
font-family: var(--paper-font-body1_-_font-family); display: block;
-webkit-font-smoothing: var( padding-top: 4px;
--paper-font-body1_-_-webkit-font-smoothing font-family: var(
); --mdc-typography-body2-font-family,
font-size: var(--paper-font-body1_-_font-size); var(--mdc-typography-font-family, Roboto, sans-serif)
font-weight: var(--paper-font-body1_-_font-weight);
line-height: var(--paper-font-body1_-_line-height);
color: var(
--paper-item-body-secondary-color,
var(--secondary-text-color)
); );
-webkit-font-smoothing: antialiased;
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
font-weight: var(--mdc-typography-body2-font-weight, 400);
line-height: normal;
color: var(--secondary-text-color);
} }
.body[two-line] { .body[two-line] {
min-height: calc( min-height: calc(

View File

@ -810,6 +810,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
} }
tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML; tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML;
tooltip.style.display = "block"; tooltip.style.display = "block";
tooltip.style.position = "fixed";
tooltip.style.top = `${top}px`; tooltip.style.top = `${top}px`;
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`; tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`;
} }
@ -840,6 +841,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
haStyleScrollbar, haStyleScrollbar,
css` css`
:host { :host {
overflow: visible;
height: 100%; height: 100%;
display: block; display: block;
overflow: hidden; overflow: hidden;

View File

@ -0,0 +1,156 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import { listSTTEngines, STTEngine } from "../data/stt";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" };
@customElement("ha-stt-picker")
export class HaSTTPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public language?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _engines?: STTEngine[];
protected render() {
if (!this._engines) {
return nothing;
}
const value =
this.value ??
(this.required
? this._engines.find(
(engine) => engine.supported_languages?.length !== 0
)
: NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.stt-picker.stt")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
let label = engine.engine_id;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else if (engine.engine_id in NAME_MAP) {
label = NAME_MAP[engine.engine_id];
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateEngines();
} else if (changedProperties.has("language")) {
this._debouncedUpdateEngines();
}
}
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (
await listSTTEngines(
this.hass,
this.language,
this.hass.config.country || undefined
)
).providers;
if (!this.value) {
return;
}
const selectedEngine = this._engines.find(
(engine) => engine.engine_id === this.value
);
fireEvent(this, "supported-languages-changed", {
value: selectedEngine?.supported_languages,
});
if (!selectedEngine || selectedEngine.supported_languages?.length === 0) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)
?.supported_languages,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-stt-picker": HaSTTPicker;
}
}

View File

@ -1,4 +1,3 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@ -1,43 +1,19 @@
import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-bar-fixed-base"; import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-bar-fixed-base";
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css"; import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
import { css } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement } from "lit/decorators";
let drawerContent: HTMLElement | undefined;
@customElement("ha-top-app-bar-fixed") @customElement("ha-top-app-bar-fixed")
export class HaTopAppBarFixed extends TopAppBarFixedBase { export class HaTopAppBarFixed extends TopAppBarFixedBase {
private get _drawerContent() {
if (!drawerContent) {
drawerContent = document
.querySelector("home-assistant")!
.renderRoot.querySelector("home-assistant-main")!
.renderRoot.querySelector("ha-drawer")!
.renderRoot.querySelector(".mdc-drawer-app-content") as HTMLElement;
}
return drawerContent;
}
@property({ type: Object })
get scrollTarget() {
return this._scrollTarget || this._drawerContent || window;
}
protected updateRootPosition() {}
static override styles = [ static override styles = [
styles, styles,
css` css`
.mdc-top-app-bar {
position: sticky;
top: 0;
}
.mdc-top-app-bar__row { .mdc-top-app-bar__row {
height: var(--header-height); height: var(--header-height);
border-bottom: var(--app-header-border-bottom); border-bottom: var(--app-header-border-bottom);
} }
.mdc-top-app-bar--fixed-adjust { .mdc-top-app-bar--fixed-adjust {
padding-top: 0; padding-top: var(--header-height);
} }
.mdc-top-app-bar { .mdc-top-app-bar {
--mdc-typography-headline6-font-weight: 400; --mdc-typography-headline6-font-weight: 400;

View File

@ -0,0 +1,156 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import { listTTSEngines, TTSEngine } from "../data/tts";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" };
@customElement("ha-tts-picker")
export class HaTTSPicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public language?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _engines?: TTSEngine[];
protected render() {
if (!this._engines) {
return nothing;
}
const value =
this.value ??
(this.required
? this._engines.find(
(engine) => engine.supported_languages?.length !== 0
)
: NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.tts-picker.tts")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
let label = engine.engine_id;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else if (engine.engine_id in NAME_MAP) {
label = NAME_MAP[engine.engine_id];
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateEngines();
} else if (changedProperties.has("language")) {
this._debouncedUpdateEngines();
}
}
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (
await listTTSEngines(
this.hass,
this.language,
this.hass.config.country || undefined
)
).providers;
if (!this.value) {
return;
}
const selectedEngine = this._engines.find(
(engine) => engine.engine_id === this.value
);
fireEvent(this, "supported-languages-changed", {
value: selectedEngine?.supported_languages,
});
if (!selectedEngine || selectedEngine.supported_languages?.length === 0) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)
?.supported_languages,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tts-picker": HaTTSPicker;
}
}

View File

@ -0,0 +1,147 @@
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import { listTTSVoices, TTSVoice } from "../data/tts";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@customElement("ha-tts-voice-picker")
export class HaTTSVoicePicker extends LitElement {
@property() public value?: string;
@property() public label?: string;
@property() public engineId?: string;
@property() public language?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = false;
@state() _voices?: TTSVoice[] | null;
@query("ha-select") private _select?: HaSelect;
protected render() {
if (!this._voices) {
return nothing;
}
const value =
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
return html`
<ha-select
.label=${this.label ||
this.hass!.localize("ui.components.tts-voice-picker.voice")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-voice-picker.none")}
</ha-list-item>`
: nothing}
${this._voices.map(
(voice) => html`<ha-list-item .value=${voice.voice_id}>
${voice.name}
</ha-list-item>`
)}
</ha-select>
`;
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this._updateVoices();
} else if (
changedProperties.has("language") ||
changedProperties.has("engineId")
) {
this._debouncedUpdateVoices();
}
}
private _debouncedUpdateVoices = debounce(() => this._updateVoices(), 500);
private async _updateVoices() {
if (!this.engineId || !this.language) {
this._voices = undefined;
return;
}
this._voices = (
await listTTSVoices(this.hass, this.engineId, this.language)
).voices;
if (!this.value) {
return;
}
if (
!this._voices ||
!this._voices.find((voice) => voice.voice_id === this.value)
) {
this.value = undefined;
fireEvent(this, "value-changed", { value: this.value });
}
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (
changedProperties.has("_voices") &&
this._select?.value !== this.value
) {
this._select?.layoutOptions();
fireEvent(this, "value-changed", { value: this._select?.value });
}
}
static get styles(): CSSResultGroup {
return css`
ha-select {
width: 100%;
}
`;
}
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tts-voice-picker": HaTTSVoicePicker;
}
}

View File

@ -1,5 +1,5 @@
import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml"; import { DEFAULT_SCHEMA, dump, load, Schema } from "js-yaml";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) public autoUpdate = false;
@property({ type: Boolean }) public readOnly = false; @property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@ -41,7 +43,11 @@ export class HaYamlEditor extends LitElement {
try { try {
this._yaml = this._yaml =
value && !isEmpty(value) value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema, quotingType: '"' }) ? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: ""; : "";
} catch (err: any) { } catch (err: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -56,6 +62,13 @@ export class HaYamlEditor extends LitElement {
} }
} }
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (this.autoUpdate && changedProperties.has("value")) {
this.setValue(this.value);
}
}
protected render() { protected render() {
if (this._yaml === undefined) { if (this._yaml === undefined) {
return nothing; return nothing;

328
src/data/assist_pipeline.ts Normal file
View File

@ -0,0 +1,328 @@
import type { HomeAssistant } from "../types";
import type { ConversationResult } from "./conversation";
import type { ResolvedMediaSource } from "./media_source";
import type { SpeechMetadata } from "./stt";
export interface AssistPipeline {
id: string;
name: string;
language: string;
conversation_engine: string;
conversation_language: string | null;
stt_engine: string | null;
stt_language: string | null;
tts_engine: string | null;
tts_language: string | null;
tts_voice: string | null;
}
export interface AssistPipelineMutableParams {
name: string;
language: string;
conversation_engine: string;
conversation_language: string | null;
stt_engine: string | null;
stt_language: string | null;
tts_engine: string | null;
tts_language: string | null;
tts_voice: string | null;
}
export interface assistRunListing {
pipeline_run_id: string;
timestamp: string;
}
interface PipelineEventBase {
timestamp: string;
}
interface PipelineRunStartEvent extends PipelineEventBase {
type: "run-start";
data: {
pipeline: string;
language: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
type: "run-end";
data: Record<string, never>;
}
interface PipelineErrorEvent extends PipelineEventBase {
type: "error";
data: {
code: string;
message: string;
};
}
interface PipelineSTTStartEvent extends PipelineEventBase {
type: "stt-start";
data: {
engine: string;
metadata: SpeechMetadata;
};
}
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
stt_output: { text: string };
};
}
interface PipelineIntentStartEvent extends PipelineEventBase {
type: "intent-start";
data: {
engine: string;
language: string;
intent_input: string;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
intent_output: ConversationResult;
};
}
interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
language: string;
voice: string;
tts_input: string;
};
}
interface PipelineTTSEndEvent extends PipelineEventBase {
type: "tts-end";
data: {
tts_output: ResolvedMediaSource;
};
}
export type PipelineRunEvent =
| PipelineRunStartEvent
| PipelineRunEndEvent
| PipelineErrorEvent
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;
export type PipelineRunOptions = (
| {
start_stage: "intent" | "tts";
input: { text: string };
}
| {
start_stage: "stt";
input: { sample_rate: number };
}
) & {
end_stage: "stt" | "intent" | "tts";
pipeline?: string;
conversation_id?: string | null;
};
export interface PipelineRun {
init_options?: PipelineRunOptions;
events: PipelineRunEvent[];
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
stt?: PipelineSTTStartEvent["data"] &
Partial<PipelineSTTEndEvent["data"]> & { done: boolean };
intent?: PipelineIntentStartEvent["data"] &
Partial<PipelineIntentEndEvent["data"]> & { done: boolean };
tts?: PipelineTTSStartEvent["data"] &
Partial<PipelineTTSEndEvent["data"]> & { done: boolean };
}
export const processEvent = (
run: PipelineRun | undefined,
event: PipelineRunEvent,
options?: PipelineRunOptions
): PipelineRun | undefined => {
if (event.type === "run-start") {
run = {
init_options: options,
stage: "ready",
run: event.data,
events: [event],
};
return run;
}
if (!run) {
// eslint-disable-next-line no-console
console.warn("Received unexpected event before receiving session", event);
return undefined;
}
if (event.type === "stt-start") {
run = {
...run,
stage: "stt",
stt: { ...event.data, done: false },
};
} else if (event.type === "stt-end") {
run = {
...run,
stt: { ...run.stt!, ...event.data, done: true },
};
} else if (event.type === "intent-start") {
run = {
...run,
stage: "intent",
intent: { ...event.data, done: false },
};
} else if (event.type === "intent-end") {
run = {
...run,
intent: { ...run.intent!, ...event.data, done: true },
};
} else if (event.type === "tts-start") {
run = {
...run,
stage: "tts",
tts: { ...event.data, done: false },
};
} else if (event.type === "tts-end") {
run = {
...run,
tts: { ...run.tts!, ...event.data, done: true },
};
} else if (event.type === "run-end") {
run = { ...run, stage: "done" };
} else if (event.type === "error") {
run = { ...run, stage: "error", error: event.data };
} else {
run = { ...run };
}
run.events = [...run.events, event];
return run;
};
export const runDebugAssistPipeline = (
hass: HomeAssistant,
callback: (run: PipelineRun) => void,
options: PipelineRunOptions
) => {
let run: PipelineRun | undefined;
const unsubProm = runAssistPipeline(
hass,
(updateEvent) => {
run = processEvent(run, updateEvent, options);
if (updateEvent.type === "run-end" || updateEvent.type === "error") {
unsubProm.then((unsub) => unsub());
}
if (run) {
callback(run);
}
},
options
);
return unsubProm;
};
export const runAssistPipeline = (
hass: HomeAssistant,
callback: (event: PipelineRunEvent) => void,
options: PipelineRunOptions
) =>
hass.connection.subscribeMessage<PipelineRunEvent>(callback, {
...options,
type: "assist_pipeline/run",
});
export const listAssistPipelineRuns = (
hass: HomeAssistant,
pipeline_id: string
) =>
hass.callWS<{
pipeline_runs: assistRunListing[];
}>({
type: "assist_pipeline/pipeline_debug/list",
pipeline_id,
});
export const getAssistPipelineRun = (
hass: HomeAssistant,
pipeline_id: string,
pipeline_run_id: string
) =>
hass.callWS<{
events: PipelineRunEvent[];
}>({
type: "assist_pipeline/pipeline_debug/get",
pipeline_id,
pipeline_run_id,
});
export const listAssistPipelines = (hass: HomeAssistant) =>
hass.callWS<{
pipelines: AssistPipeline[];
preferred_pipeline: string | null;
}>({
type: "assist_pipeline/pipeline/list",
});
export const getAssistPipeline = (hass: HomeAssistant, pipeline_id?: string) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/get",
pipeline_id,
});
export const createAssistPipeline = (
hass: HomeAssistant,
pipeline: AssistPipelineMutableParams
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/create",
...pipeline,
});
export const updateAssistPipeline = (
hass: HomeAssistant,
pipeline_id: string,
pipeline: AssistPipelineMutableParams
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/update",
pipeline_id,
...pipeline,
});
export const setAssistPipelinePreferred = (
hass: HomeAssistant,
pipeline_id: string
) =>
hass.callWS({
type: "assist_pipeline/pipeline/set_preferred",
pipeline_id,
});
export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) =>
hass.callWS<void>({
type: "assist_pipeline/pipeline/delete",
pipeline_id: pipelineId,
});
export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
hass.callWS<{ languages: string[] }>({
type: "assist_pipeline/language/list",
});

View File

@ -123,6 +123,8 @@ export interface TimePatternTrigger extends BaseTrigger {
export interface WebhookTrigger extends BaseTrigger { export interface WebhookTrigger extends BaseTrigger {
platform: "webhook"; platform: "webhook";
webhook_id: string; webhook_id: string;
allowed_methods?: string[];
local_only?: boolean;
} }
export interface ZoneTrigger extends BaseTrigger { export interface ZoneTrigger extends BaseTrigger {

View File

@ -9,15 +9,6 @@ interface CloudStatusNotLoggedIn {
http_use_ssl: boolean; http_use_ssl: boolean;
} }
export interface GoogleEntityConfig {
should_expose?: boolean | null;
disable_2fa?: boolean;
}
export interface AlexaEntityConfig {
should_expose?: boolean | null;
}
export interface CertificateInformation { export interface CertificateInformation {
common_name: string; common_name: string;
expire_date: string; expire_date: string;
@ -30,14 +21,6 @@ export interface CloudPreferences {
remote_enabled: boolean; remote_enabled: boolean;
google_secure_devices_pin: string | undefined; google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook }; cloudhooks: { [webhookId: string]: CloudWebhook };
google_default_expose: string[] | null;
google_entity_configs: {
[entityId: string]: GoogleEntityConfig;
};
alexa_default_expose: string[] | null;
alexa_entity_configs: {
[entityId: string]: AlexaEntityConfig;
};
alexa_report_state: boolean; alexa_report_state: boolean;
google_report_state: boolean; google_report_state: boolean;
tts_default_voice: [string, string]; tts_default_voice: [string, string];
@ -57,6 +40,13 @@ export interface CloudStatusLoggedIn {
remote_domain: string | undefined; remote_domain: string | undefined;
remote_connected: boolean; remote_connected: boolean;
remote_certificate: undefined | CertificateInformation; remote_certificate: undefined | CertificateInformation;
remote_certificate_status:
| null
| "error"
| "generating"
| "loaded"
| "loading"
| "ready";
http_use_ssl: boolean; http_use_ssl: boolean;
active_subscription: boolean; active_subscription: boolean;
} }
@ -86,10 +76,14 @@ export const cloudLogin = (
email: string, email: string,
password: string password: string
) => ) =>
hass.callApi("POST", "cloud/login", { hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
email, "POST",
password, "cloud/login",
}); {
email,
password,
}
);
export const cloudLogout = (hass: HomeAssistant) => export const cloudLogout = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/logout"); hass.callApi("POST", "cloud/logout");
@ -150,10 +144,8 @@ export const updateCloudPref = (
prefs: { prefs: {
google_enabled?: CloudPreferences["google_enabled"]; google_enabled?: CloudPreferences["google_enabled"];
alexa_enabled?: CloudPreferences["alexa_enabled"]; alexa_enabled?: CloudPreferences["alexa_enabled"];
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
alexa_report_state?: CloudPreferences["alexa_report_state"]; alexa_report_state?: CloudPreferences["alexa_report_state"];
google_report_state?: CloudPreferences["google_report_state"]; google_report_state?: CloudPreferences["google_report_state"];
google_default_expose?: CloudPreferences["google_default_expose"];
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"]; google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"]; tts_default_voice?: CloudPreferences["tts_default_voice"];
} }
@ -165,25 +157,14 @@ export const updateCloudPref = (
export const updateCloudGoogleEntityConfig = ( export const updateCloudGoogleEntityConfig = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entity_id: string,
values: GoogleEntityConfig disable_2fa: boolean
) => ) =>
hass.callWS<GoogleEntityConfig>({ hass.callWS({
type: "cloud/google_assistant/entities/update", type: "cloud/google_assistant/entities/update",
entity_id: entityId, entity_id,
...values, disable_2fa,
}); });
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) => export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync"); hass.callApi("POST", "cloud/google_actions/sync");
export const updateCloudAlexaEntityConfig = (
hass: HomeAssistant,
entityId: string,
values: AlexaEntityConfig
) =>
hass.callWS<AlexaEntityConfig>({
type: "cloud/alexa/entities/update",
entity_id: entityId,
...values,
});

View File

@ -56,6 +56,12 @@ export interface AgentInfo {
attribution?: { name: string; url: string }; attribution?: { name: string; url: string };
} }
export interface Agent {
id: string;
name: string;
supported_languages: "*" | string[];
}
export const processConversationInput = ( export const processConversationInput = (
hass: HomeAssistant, hass: HomeAssistant,
text: string, text: string,
@ -70,9 +76,24 @@ export const processConversationInput = (
language, language,
}); });
export const getAgentInfo = (hass: HomeAssistant): Promise<AgentInfo> => export const listAgents = (
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ agents: Agent[] }> =>
hass.callWS({
type: "conversation/agent/list",
language,
country,
});
export const getAgentInfo = (
hass: HomeAssistant,
agent_id?: string
): Promise<AgentInfo> =>
hass.callWS({ hass.callWS({
type: "conversation/agent/info", type: "conversation/agent/info",
agent_id,
}); });
export const prepareConversation = ( export const prepareConversation = (

View File

@ -90,6 +90,9 @@ export interface EntityRegistryOptions {
number?: NumberEntityOptions; number?: NumberEntityOptions;
sensor?: SensorEntityOptions; sensor?: SensorEntityOptions;
weather?: WeatherEntityOptions; weather?: WeatherEntityOptions;
conversation?: Record<string, unknown>;
"cloud.alexa"?: Record<string, unknown>;
"cloud.google_assistant"?: Record<string, unknown>;
} }
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {

View File

@ -9,5 +9,14 @@ export interface GoogleEntity {
export const fetchCloudGoogleEntities = (hass: HomeAssistant) => export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" }); hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
export const fetchCloudGoogleEntity = (
hass: HomeAssistant,
entity_id: string
) =>
hass.callWS<GoogleEntity>({
type: "cloud/google_assistant/entities/get",
entity_id,
});
export const syncCloudGoogleEntities = (hass: HomeAssistant) => export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync"); hass.callApi("POST", "cloud/google_actions/sync");

View File

@ -381,3 +381,23 @@ export const fetchAddonInfo = (
? `/store/addons/${addonSlug}` // Use /store/addons when add-on is not installed ? `/store/addons/${addonSlug}` // Use /store/addons when add-on is not installed
: `/addons/${addonSlug}/info` // Use /addons when add-on is installed : `/addons/${addonSlug}/info` // Use /addons when add-on is installed
); );
export const rebuildLocalAddon = async (
hass: HomeAssistant,
slug: string
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS<void>({
type: "supervisor/api",
endpoint: `/addons/${slug}/rebuild`,
method: "post",
timeout: null,
});
}
return (
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}rebuild`
)
).data;
};

View File

@ -14,6 +14,7 @@ export type Selector =
| BooleanSelector | BooleanSelector
| ColorRGBSelector | ColorRGBSelector
| ColorTempSelector | ColorTempSelector
| ConversationAgentSelector
| ConfigEntrySelector | ConfigEntrySelector
| ConstantSelector | ConstantSelector
| DateSelector | DateSelector
@ -25,19 +26,24 @@ export type Selector =
| LegacyEntitySelector | LegacyEntitySelector
| FileSelector | FileSelector
| IconSelector | IconSelector
| LanguageSelector
| LocationSelector | LocationSelector
| MediaSelector | MediaSelector
| NavigationSelector | NavigationSelector
| NumberSelector | NumberSelector
| ObjectSelector | ObjectSelector
| AssistPipelineSelector
| SelectSelector | SelectSelector
| StateSelector | StateSelector
| StatisticSelector | StatisticSelector
| StringSelector | StringSelector
| STTSelector
| TargetSelector | TargetSelector
| TemplateSelector | TemplateSelector
| ThemeSelector | ThemeSelector
| TimeSelector | TimeSelector
| TTSSelector
| TTSVoiceSelector
| UiActionSelector | UiActionSelector
| UiColorSelector; | UiColorSelector;
@ -85,6 +91,10 @@ export interface ColorTempSelector {
} | null; } | null;
} }
export interface ConversationAgentSelector {
conversation_agent: { language?: string } | null;
}
export interface ConfigEntrySelector { export interface ConfigEntrySelector {
config_entry: { config_entry: {
integration?: string; integration?: string;
@ -201,6 +211,14 @@ export interface IconSelector {
} | null; } | null;
} }
export interface LanguageSelector {
language: {
languages?: string[];
native_name?: boolean;
no_sort?: boolean;
} | null;
}
export interface LocationSelector { export interface LocationSelector {
location: { radius?: boolean; icon?: string } | null; location: { radius?: boolean; icon?: string } | null;
} }
@ -249,6 +267,11 @@ export interface ObjectSelector {
object: {} | null; object: {} | null;
} }
export interface AssistPipelineSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
assist_pipeline: {} | null;
}
export interface SelectOption { export interface SelectOption {
value: any; value: any;
label: string; label: string;
@ -294,6 +317,10 @@ export interface StringSelector {
} | null; } | null;
} }
export interface STTSelector {
stt: { language?: string } | null;
}
export interface TargetSelector { export interface TargetSelector {
target: { target: {
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
@ -315,15 +342,23 @@ export interface TimeSelector {
time: {} | null; time: {} | null;
} }
export interface TTSSelector {
tts: { language?: string } | null;
}
export interface TTSVoiceSelector {
tts_voice: { engineId?: string; language?: string } | null;
}
export interface UiActionSelector { export interface UiActionSelector {
"ui-action": { ui_action: {
actions?: UiAction[]; actions?: UiAction[];
} | null; } | null;
} }
export interface UiColorSelector { export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
"ui-color": {} | null; ui_color: {} | null;
} }
export const filterSelectorDevices = ( export const filterSelectorDevices = (

View File

@ -1,3 +1,5 @@
import { HomeAssistant } from "../types";
export interface SpeechMetadata { export interface SpeechMetadata {
language: string; language: string;
format: "wav" | "ogg"; format: "wav" | "ogg";
@ -15,3 +17,19 @@ export interface SpeechMetadata {
| 48000; | 48000;
channel: 1 | 2; channel: 1 | 2;
} }
export interface STTEngine {
engine_id: string;
supported_languages?: string[];
}
export const listSTTEngines = (
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ providers: STTEngine[] }> =>
hass.callWS({
type: "stt/engine/list",
language,
country,
});

View File

@ -1,5 +1,15 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface TTSEngine {
engine_id: string;
supported_languages?: string[];
}
export interface TTSVoice {
voice_id: string;
name: string;
}
export const convertTextToSpeech = ( export const convertTextToSpeech = (
hass: HomeAssistant, hass: HomeAssistant,
data: { data: {
@ -18,3 +28,25 @@ export const isTTSMediaSource = (mediaContentId: string) =>
export const getProviderFromTTSMediaSource = (mediaContentId: string) => export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length); mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length);
export const listTTSEngines = (
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ providers: TTSEngine[] }> =>
hass.callWS({
type: "tts/engine/list",
language,
country,
});
export const listTTSVoices = (
hass: HomeAssistant,
engine_id: string,
language: string
): Promise<{ voices: TTSVoice[] | null }> =>
hass.callWS({
type: "tts/engine/voices",
engine_id,
language,
});

43
src/data/voice.ts Normal file
View File

@ -0,0 +1,43 @@
import { HomeAssistant } from "../types";
export const voiceAssistants = {
conversation: { domain: "assist_pipeline", name: "Assist" },
"cloud.alexa": {
domain: "alexa",
name: "Amazon Alexa",
},
"cloud.google_assistant": {
domain: "google_assistant",
name: "Google Assistant",
},
} as const;
export const setExposeNewEntities = (
hass: HomeAssistant,
assistant: string,
expose_new: boolean
) =>
hass.callWS({
type: "homeassistant/expose_new_entities/set",
assistant,
expose_new,
});
export const getExposeNewEntities = (hass: HomeAssistant, assistant: string) =>
hass.callWS<{ expose_new: boolean }>({
type: "homeassistant/expose_new_entities/get",
assistant,
});
export const exposeEntities = (
hass: HomeAssistant,
assistants: string[],
entity_ids: string[],
should_expose: boolean
) =>
hass.callWS({
type: "homeassistant/expose_entity",
assistants,
entity_ids,
should_expose,
});

View File

@ -1,197 +0,0 @@
import type { HomeAssistant } from "../types";
import type { ConversationResult } from "./conversation";
import type { ResolvedMediaSource } from "./media_source";
import type { SpeechMetadata } from "./stt";
interface PipelineEventBase {
timestamp: string;
}
interface PipelineRunStartEvent extends PipelineEventBase {
type: "run-start";
data: {
pipeline: string;
language: string;
runner_data: {
stt_binary_handler_id: number | null;
timeout: number;
};
};
}
interface PipelineRunEndEvent extends PipelineEventBase {
type: "run-end";
data: Record<string, never>;
}
interface PipelineErrorEvent extends PipelineEventBase {
type: "error";
data: {
code: string;
message: string;
};
}
interface PipelineSTTStartEvent extends PipelineEventBase {
type: "stt-start";
data: {
engine: string;
metadata: SpeechMetadata;
};
}
interface PipelineSTTEndEvent extends PipelineEventBase {
type: "stt-end";
data: {
stt_output: { text: string };
};
}
interface PipelineIntentStartEvent extends PipelineEventBase {
type: "intent-start";
data: {
engine: string;
intent_input: string;
};
}
interface PipelineIntentEndEvent extends PipelineEventBase {
type: "intent-end";
data: {
intent_output: ConversationResult;
};
}
interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
tts_input: string;
};
}
interface PipelineTTSEndEvent extends PipelineEventBase {
type: "tts-end";
data: {
tts_output: ResolvedMediaSource;
};
}
type PipelineRunEvent =
| PipelineRunStartEvent
| PipelineRunEndEvent
| PipelineErrorEvent
| PipelineSTTStartEvent
| PipelineSTTEndEvent
| PipelineIntentStartEvent
| PipelineIntentEndEvent
| PipelineTTSStartEvent
| PipelineTTSEndEvent;
export interface PipelineRunOptions {
start_stage: "stt" | "intent" | "tts";
end_stage: "stt" | "intent" | "tts";
language?: string;
pipeline?: string;
input?: { text: string };
conversation_id?: string | null;
}
export interface PipelineRun {
init_options: PipelineRunOptions;
events: PipelineRunEvent[];
stage: "ready" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
stt?: PipelineSTTStartEvent["data"] &
Partial<PipelineSTTEndEvent["data"]> & { done: boolean };
intent?: PipelineIntentStartEvent["data"] &
Partial<PipelineIntentEndEvent["data"]> & { done: boolean };
tts?: PipelineTTSStartEvent["data"] &
Partial<PipelineTTSEndEvent["data"]> & { done: boolean };
}
export const runVoiceAssistantPipeline = (
hass: HomeAssistant,
callback: (event: PipelineRun) => void,
options: PipelineRunOptions
) => {
let run: PipelineRun | undefined;
const unsubProm = hass.connection.subscribeMessage<PipelineRunEvent>(
(updateEvent) => {
if (updateEvent.type === "run-start") {
run = {
init_options: options,
stage: "ready",
run: updateEvent.data,
error: undefined,
stt: undefined,
intent: undefined,
tts: undefined,
events: [updateEvent],
};
callback(run);
return;
}
if (!run) {
// eslint-disable-next-line no-console
console.warn(
"Received unexpected event before receiving session",
updateEvent
);
return;
}
if (updateEvent.type === "stt-start") {
run = {
...run,
stage: "stt",
stt: { ...updateEvent.data, done: false },
};
} else if (updateEvent.type === "stt-end") {
run = {
...run,
stt: { ...run.stt!, ...updateEvent.data, done: true },
};
} else if (updateEvent.type === "intent-start") {
run = {
...run,
stage: "intent",
intent: { ...updateEvent.data, done: false },
};
} else if (updateEvent.type === "intent-end") {
run = {
...run,
intent: { ...run.intent!, ...updateEvent.data, done: true },
};
} else if (updateEvent.type === "tts-start") {
run = {
...run,
stage: "tts",
tts: { ...updateEvent.data, done: false },
};
} else if (updateEvent.type === "tts-end") {
run = {
...run,
tts: { ...run.tts!, ...updateEvent.data, done: true },
};
} else if (updateEvent.type === "run-end") {
run = { ...run, stage: "done" };
unsubProm.then((unsub) => unsub());
} else if (updateEvent.type === "error") {
run = { ...run, stage: "error", error: updateEvent.data };
unsubProm.then((unsub) => unsub());
} else {
run = { ...run };
}
run.events = [...run.events, updateEvent];
callback(run);
},
{
...options,
type: "voice_assistant/run",
}
);
return unsubProm;
};

View File

@ -1,16 +1,13 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiDeleteOutline, mdiPlus } from "@mdi/js"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert"; import "../../components/ha-alert";
import "../../components/ha-area-picker";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import { haStyle, haStyleDialog } from "../../resources/styles"; import { haStyle, haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { AliasesDialogParams } from "./show-dialog-aliases"; import { AliasesDialogParams } from "./show-dialog-aliases";
import "../../components/ha-aliases-editor";
@customElement("dialog-aliases") @customElement("dialog-aliases")
class DialogAliases extends LitElement { class DialogAliases extends LitElement {
@ -57,43 +54,11 @@ class DialogAliases extends LitElement {
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}
<div class="form"> <ha-aliases-editor
${this._aliases.map( .hass=${this.hass}
(alias, index) => html` .aliases=${this._aliases}
<div class="layout horizontal center-center row"> @value-changed=${this._aliasesChanged}
<ha-textfield ></ha-aliases-editor>
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
.label=${this.hass!.localize(
"ui.dialogs.aliases.input_label",
{ number: index + 1 }
)}
.value=${alias}
?data-last=${index === this._aliases.length - 1}
@input=${this._editAlias}
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize(
"ui.dialogs.aliases.remove_alias",
{ number: index + 1 }
)}
@click=${this._removeAlias}
.path=${mdiDeleteOutline}
></ha-icon-button>
</div>
`
)}
<div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}>
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>
</div>
</div>
</div> </div>
<mwc-button <mwc-button
slot="secondaryAction" slot="secondaryAction"
@ -113,32 +78,8 @@ class DialogAliases extends LitElement {
`; `;
} }
private async _addAlias() { private _aliasesChanged(ev: CustomEvent): void {
this._aliases = [...this._aliases, ""]; this._aliases = ev.detail.value;
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
| HaTextField
| undefined;
field?.focus();
}
private async _editAlias(ev: Event) {
const index = (ev.target as any).index;
this._aliases[index] = (ev.target as any).value;
}
private async _keyDownAlias(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._addAlias();
}
}
private async _removeAlias(ev: Event) {
const index = (ev.target as any).index;
const aliases = [...this._aliases];
aliases.splice(index, 1);
this._aliases = aliases;
} }
private async _updateAliases(): Promise<void> { private async _updateAliases(): Promise<void> {

View File

@ -1,117 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import { domainToName } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler")
class DomainTogglerDialog
extends LitElement
implements HassDialog<HaDomainTogglerDialogParams>
{
public hass!: HomeAssistant;
@state() private _params?: HaDomainTogglerDialogParams;
public showDialog(params: HaDomainTogglerDialogParams): void {
this._params = params;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
const domains = this._params.domains
.map((domain) => [domainToName(this.hass.localize, domain), domain])
.sort();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this._params.title ||
this.hass.localize("ui.dialogs.domain_toggler.title")
)}
>
${this._params.description
? html`<div class="description">${this._params.description}</div>`
: ""}
<div class="domains">
${domains.map(
(domain) =>
html`
<ha-formfield .label=${domain[0]}>
<ha-switch
.domain=${domain[1]}
.checked=${!this._params!.exposedDomains ||
this._params!.exposedDomains.includes(domain[1])}
@change=${this._handleSwitch}
>
</ha-switch>
</ha-formfield>
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
${this.hass.localize(
"ui.dialogs.domain_toggler.reset_entities"
)}
</mwc-button>
`
)}
</div>
</ha-dialog>
`;
}
private _handleSwitch(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
ev.currentTarget.blur();
}
private _handleReset(ev) {
this._params!.resetDomain(ev.currentTarget.domain);
ev.currentTarget.blur();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
.description {
margin-bottom: 8px;
}
.domains {
display: grid;
grid-template-columns: auto auto;
grid-row-gap: 8px;
align-items: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-domain-toggler": DomainTogglerDialog;
}
}

View File

@ -1,23 +0,0 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface HaDomainTogglerDialogParams {
title?: string;
description?: string;
domains: string[];
exposedDomains: string[] | null;
toggleDomain: (domain: string, turnOn: boolean) => void;
resetDomain: (domain: string) => void;
}
export const loadDomainTogglerDialog = () => import("./dialog-domain-toggler");
export const showDomainTogglerDialog = (
element: HTMLElement,
dialogParams: HaDomainTogglerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-domain-toggler",
dialogImport: loadDomainTogglerDialog,
dialogParams,
});
};

View File

@ -161,8 +161,6 @@ class DialogBox extends LitElement {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
ha-dialog { ha-dialog {
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
/* Place above other dialogs */ /* Place above other dialogs */
--dialog-z-index: 104; --dialog-z-index: 104;
} }

View File

@ -176,8 +176,6 @@ export class DialogEnterCode
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
ha-dialog { ha-dialog {
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
/* Place above other dialogs */ /* Place above other dialogs */
--dialog-z-index: 104; --dialog-z-index: 104;
} }

View File

@ -569,6 +569,7 @@ class MoreInfoViewLightColorPicker extends LitElement {
line-height: 24px; line-height: 24px;
letter-spacing: 0.1px; letter-spacing: 0.1px;
margin: 0; margin: 0;
direction: ltr;
} }
.color-temp { .color-temp {

View File

@ -0,0 +1,49 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ExtEntityRegistryEntry } from "../../../../data/entity_registry";
import "../../../../panels/config/voice-assistants/entity-voice-settings";
import { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-view-voice-assistants")
class MoreInfoViewVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@property() public params?;
protected render() {
if (!this.params) {
return nothing;
}
return html`<entity-voice-settings
.hass=${this.hass}
.entry=${this.entry}
></entity-voice-settings>`;
}
static get styles(): CSSResultGroup {
return [
css`
:host {
display: flex;
flex-direction: column;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
flex: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-voice-assistants": MoreInfoViewVoiceAssistants;
}
}

View File

@ -0,0 +1,16 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadVoiceAssistantsView = () =>
import("./ha-more-info-view-voice-assistants");
export const showVoiceAssistantsView = (
element: HTMLElement,
title: string
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-voice-assistants",
viewImport: loadVoiceAssistantsView,
viewTitle: title,
viewParams: {},
});
};

View File

@ -181,10 +181,10 @@ export class MoreInfoDialog extends LitElement {
this.setView("settings"); this.setView("settings");
} }
private async _showChildView(ev: CustomEvent): Promise<void> { private _showChildView(ev: CustomEvent): void {
const view = ev.detail as ChildView; const view = ev.detail as ChildView;
if (view.viewImport) { if (view.viewImport) {
await view.viewImport(); view.viewImport();
} }
this._childView = view; this._childView = view;
} }
@ -369,12 +369,14 @@ export class MoreInfoDialog extends LitElement {
tabindex="-1" tabindex="-1"
dialogInitialFocus dialogInitialFocus
@show-child-view=${this._showChildView} @show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
> >
${this._childView ${this._childView
? html` ? html`
<div class="child-view"> <div class="child-view">
${dynamicElement(this._childView.viewTag, { ${dynamicElement(this._childView.viewTag, {
hass: this.hass, hass: this.hass,
entry: this._entry,
params: this._childView.viewParams, params: this._childView.viewParams,
})} })}
</div> </div>
@ -401,7 +403,6 @@ export class MoreInfoDialog extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
.entry=${this._entry} .entry=${this._entry}
@entity-entry-updated=${this._entryUpdated}
></ha-more-info-settings> ></ha-more-info-settings>
` `
: this._currView === "related" : this._currView === "related"

View File

@ -27,7 +27,7 @@ declare global {
} }
} }
const statTypes: StatisticsTypes = ["state", "min", "mean", "max"]; const statTypes: StatisticsTypes = ["min", "mean", "max"];
@customElement("ha-more-info-history") @customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement { export class MoreInfoHistory extends LitElement {

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