Compare commits

...

99 Commits

Author SHA1 Message Date
Bram Kragten
d0c7f65256 20230501.0 (#16364) 2023-05-01 19:55:38 +02:00
Bram Kragten
f99f554f19 Bumped version to 20230501.0 2023-05-01 19:54:40 +02:00
Bram Kragten
e069b5eed1 Update en.json 2023-05-01 17:15:26 +02:00
Allen Porter
3a481ebb1a Fix edits for single instance of all day recurring event (#16354) 2023-05-01 14:48:57 +02:00
Steve Repsher
a209fadf18 List Core JS polyfills for browserslist environments (#16356) 2023-05-01 14:47:57 +02:00
Bram Kragten
9f1bd1e085 Fix unused entities (#16359) 2023-05-01 14:46:54 +02:00
Bram Kragten
edc6da04f7 Make sure stt_binary_handler_id is reset (#16360) 2023-05-01 14:46:44 +02:00
Bram Kragten
2fb1dd0ec1 Add icon at unsupported message in voice settings (#16358) 2023-05-01 14:28:26 +02:00
Paul Bottein
3f2aac0842 Fix cloud subscription message in pipeline creation (#16348) 2023-04-28 17:03:34 -04:00
Paul Bottein
71dd822978 20230428.0 (#16347) 2023-04-28 17:40:58 +02:00
Paul Bottein
d1877595a5 Bumped version to 20230428.0 2023-04-28 17:19:43 +02:00
Paul Bottein
6379713f57 Fix drag and drop with sortablejs (#16343) 2023-04-28 14:40:01 +02:00
Paul Bottein
3b33195ff6 Add camera view support to image element (#16346) 2023-04-28 14:17:33 +02:00
c0ffeeca7
c7f1f1bcd1 Fix typo (#16341)
* Fix typo

* Fix typo
2023-04-28 08:01:24 +00:00
Paul Bottein
c50aad8403 20230427.0 (#16338) 2023-04-27 17:31:29 +02:00
Paul Bottein
fac4795f14 Bumped version to 20230427.0 2023-04-27 17:16:04 +02:00
Bram Kragten
062e402ef1 Show if entity is supported in expose list (#16335)
* Show if entity is supported in expose list

* Add translations and refactor code

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-04-27 17:12:12 +02:00
Bram Kragten
29be64a858 Add not supported warning in entity voice settings (#16336)
* Add not supported warning in entity voice settings

* Add alexa support and improve style

* Only toggle supported

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-04-27 17:11:49 +02:00
Paul Bottein
b3b74b8328 Move debug and preferred button to top (#16337) 2023-04-27 15:02:57 +02:00
Paul Bottein
37ba34cb0d Virtualize the add exposed entity list (#16333) 2023-04-27 13:22:37 +02:00
dependabot[bot]
8ecdde3507 Bump yaml from 2.2.1 to 2.2.2 (#16310) 2023-04-27 10:12:03 +02:00
Paul Bottein
04d34aa80c Improve search and filtering in expose entity page (#16330) 2023-04-27 09:50:19 +02:00
J. Nick Koston
26bb1ba146 Revert "Avoid fetching unused stats state column for more info" (#16328)
Revert "Avoid fetching unused stats state column for more info (#16141)"

This reverts commit 49a14a7265.
2023-04-27 09:31:06 +02:00
Bram Kragten
b7667d2cbf 20230426.0 (#16327) 2023-04-26 18:31:51 +02:00
Bram Kragten
0c36600a81 Merge branch 'master' into dev 2023-04-26 18:17:33 +02:00
Bram Kragten
feaf61a0ae Bumped version to 20230426.0 2023-04-26 18:15:53 +02:00
Bram Kragten
3e2844a65a Only show assistants that are active (#16325)
* Only show assistants that are active

* also use in texts

* Add entity id

* Update ha-config-voice-assistants-expose.ts

* remove voiceAssistantKeys

* Update entity-voice-settings.ts

* update styling

* search case
2023-04-26 18:15:10 +02:00
Paul Bottein
3a2d7baa25 Fix empty translation (#16326) 2023-04-26 18:14:57 +02:00
Paul Bottein
349cca5ff2 Update icons to expose/unexpose entity (#16323) 2023-04-26 17:08:15 +02:00
Paul Bottein
5cd3ce66f6 Keep area field next to use device area field (#16324) 2023-04-26 14:54:59 +00:00
Bram Kragten
8cf8c41698 Show number of exposed entities (#16321) 2023-04-26 14:23:23 +00:00
Bram Kragten
ff4c01e15c Replace paper-item in integration card (#16317) 2023-04-26 16:11:54 +02:00
Paul Bottein
addb66f21d Inform user that subscription is needed for cloud services (#16318) 2023-04-26 13:42:44 +00:00
Bram Kragten
a5759e36b2 Fix helper entity settings (removing) (#16320) 2023-04-26 15:29:59 +02:00
Bram Kragten
9852186ff7 Update my link for voice assistants (#16319
Making it consistant with others and match

https://github.com/home-assistant/my.home-assistant.io/pull/353
2023-04-26 14:51:31 +02:00
Bram Kragten
19c9486351 Update entity settings, combine basic editor with normal editor (#16297) 2023-04-26 12:23:53 +02:00
Bram Kragten
8c06712ab7 Allow to select a pipeline in voice command dialog (#16291) 2023-04-26 12:10:17 +02:00
Bram Kragten
63de324224 Fix my link add integration: search for discovered items (#16315)
* fix my link add integration

* add confirm back

* Update dialog-add-integration.ts
2023-04-26 12:02:50 +02:00
Bram Kragten
10d476195d Ask to use cloud pipeline as preferred (#16286)
* Ask to use cloud pipeline as preferred

* Update
2023-04-26 00:36:51 -04:00
Johan Frick
e0c4b85ef1 Fix options typo for ha-form in gallery (#16305) 2023-04-25 19:59:58 +00:00
Bram Kragten
3441a86613 Update general step description of pipeline dialog (#16307)
update general step description of pipeline dialog
2023-04-25 21:44:51 +02:00
Paul Bottein
643b168c69 Add icon and state formatting for tts and stt domain (#16304) 2023-04-25 21:40:22 +02:00
Paul Bottein
db0e5a8a41 Add translations to pipeline UI (#16303) 2023-04-25 20:01:48 +02:00
Ben Randall
64a693332b Fix height of main automation trace element (#16283)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2023-04-25 13:46:15 +00:00
Bram Kragten
327927baa7 Update expose mobile view (#16302) 2023-04-25 13:33:21 +00:00
karwosts
ce8fc17ef8 Markdown feature parity for blueprint scripts vs automations (#16250) 2023-04-25 15:31:52 +02:00
Paul Bottein
62ed1d54b0 Voice assistant cloud upsell (#16299) 2023-04-25 12:41:53 +02:00
Bram Kragten
2498f1db41 Fix tts in voice command for iOS (#16300) 2023-04-25 10:02:31 +00:00
Bram Kragten
8a50bb058d Fix audio playing on iOS (#16298) 2023-04-25 09:38:14 +00:00
Bram Kragten
e793675c47 Fix dialog text colors theme (#16296)
* Fix dialog text colors theme

* fix drawer border
2023-04-25 00:23:08 +02:00
Paul Bottein
07cef18918 Try TTS in pipeline form (#16292) 2023-04-24 19:39:16 +02:00
Bram Kragten
a0263f25c4 Improve tts playback in voice command dialog (#16288) 2023-04-24 18:45:00 +02:00
Paul Bottein
52546ab567 Fix media browser bar position (#16293)
Fixes media browser bar position
2023-04-24 18:09:21 +02:00
Bram Kragten
c0ec7e4f09 Remove wrong language from stt event (#16285) 2023-04-24 18:08:36 +02:00
renovate[bot]
8b61390e19 Update vaadinWebComponents monorepo to v23.3.11 (#16289) 2023-04-24 11:55:11 +00:00
Steve Repsher
9ba114777e Add browserslist config and use for Babel preset environment (#16267) 2023-04-24 11:28:27 +02:00
Steve Repsher
4e1e76ccc2 Add module preload to demo page (#16274) 2023-04-24 11:27:40 +02:00
dependabot[bot]
609300f40b Bump home-assistant/wheels from 2022.10.1 to 2023.04.0 (#16284)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-24 11:10:41 +02:00
renovate[bot]
eac9ac4757 Update dependency google-timezones-json to v1.1.0 (#16282) 2023-04-23 19:37:23 -04:00
Bram Kragten
708d1b81da Mark lang and country required in onboarding (#16277) 2023-04-23 19:33:44 -04:00
Bram Kragten
35baf4c779 Fix audio recorder (#16280)
FIx audio recorder
2023-04-23 13:48:46 -04:00
renovate[bot]
8d1ae71741 Update dependency sinon to v15.0.4 (#16278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-23 13:20:24 -04:00
Bram Kragten
9b32c9c6b4 Voice picker: Guard for select undefined, update debug pipelines (#16279)
* Voice picker: Guard for select undefined

* Update types, make debug a little nicer

* Add type to stt-start event

* Add language to STT data in render pipeline

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-04-23 17:04:43 +00:00
renovate[bot]
439f34f724 Update dependency @codemirror/commands to v6.2.3 (#16276) 2023-04-22 16:50:59 +00:00
Bram Kragten
cef3b99e16 fix add assist dialog (#16275) 2023-04-22 12:43:04 -04:00
renovate[bot]
9dbdf611c5 Update formatjs monorepo (#16273) 2023-04-22 12:31:03 -04:00
Bram Kragten
86f8d2d737 remove underline, format language 2023-04-22 15:42:46 +02:00
Bram Kragten
1ded47d368 Use assist_pipeline in voice command dialog (#16257)
* Remove speech recognition, add basic pipeline support

* Add basic voice support

* cleanup

* only use tts if pipeline supports it

* Update ha-voice-command-dialog.ts

* Fix types

* handle stop during stt

* Revert "Fix types"

This reverts commit 741781e392.

* active read only

* Update ha-voice-command-dialog.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-04-21 21:50:30 -04:00
Bram Kragten
85a27e8bb1 Update add assistant dialog (#16266)
* Update add assistant dialog

* fix default agent to ha when no supported

* Update ha-tts-voice-picker.ts

* Update assist-pipeline-detail-conversation.ts

* Update ha-dialog.ts

* dont override config

* Update ha-language-picker.ts
2023-04-21 20:41:30 -04:00
renovate[bot]
878f3b8df4 Update typescript-eslint monorepo to v5.59.0 (#16271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-21 23:26:24 +00:00
renovate[bot]
50c25a8276 Update dependency glob to v10.2.1 (#16268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-21 19:07:25 -04:00
renovate[bot]
c0de3b8269 Update formatjs monorepo (#16254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-21 13:15:18 -04:00
Bram Kragten
09f4e19d4c Pipelines: Add voice selector, implement supported languages (#16261) 2023-04-20 22:53:05 +02:00
Bram Kragten
6e91ac2a34 fix css selector 2023-04-20 18:53:28 +02:00
Paul Bottein
49b0c7c3d1 Move error outside ha form for assist pipeline form (#16258) 2023-04-20 18:49:27 +02:00
Paul Bottein
be1867900e Use supported_languages for stt, tts and conversation (#16256)
* Use supported_languages for stt, tts and conversation

* Fix disabled condition
2023-04-20 18:01:42 +02:00
Yosi Levy
a43f49f4af Fix some RTL aligments (#16252) 2023-04-20 17:05:24 +02:00
Paul Bottein
ea0f29782d Assist pipeline language voice (#16255)
* Update types

* Split form into multiple components

* Improve design

* Send all data

* Update wording
2023-04-20 11:02:48 -04:00
Paul Bottein
65161ce581 Language selector (#16253)
* Add language selector

* Use intl display names

* Use language picker in general settings and profile

* Add nativeName option

* Add format language util

* Add display-name polyfill

* Add native name to selector

* Rename variable
2023-04-20 10:12:49 -04:00
Bram Kragten
088cc69083 Add pipeline picker/selector (#16224)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-04-20 09:41:47 +00:00
Bram Kragten
be005b4c88 Add language to conversation agent picker (#16244) 2023-04-20 11:26:42 +02:00
Steve Repsher
aac28efd32 Streamline HTML generation and consolidate templates (#16117) 2023-04-20 11:10:12 +02:00
Bram Kragten
0d020e0300 Fix stt/tts pickers (#16241) 2023-04-20 11:03:47 +02:00
Steve Repsher
eeb84f65b9 Remove unnecessary Babel plugins from dependencies (#16251) 2023-04-20 11:00:04 +02:00
Steve Repsher
22f5d6cacb Migrate Babel loose options to assumptions (#16245) 2023-04-20 10:59:08 +02:00
renovate[bot]
52a7b41096 Lock file maintenance (#16248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-20 03:18:13 +00:00
renovate[bot]
f9f87d1147 Lock file maintenance (#16246) 2023-04-19 19:00:16 -04:00
Paul Bottein
0b3dff00df Revert "Add language selector" (#16247)
Revert "Add language selector (#16242)"

This reverts commit d89ac0f30d.
2023-04-19 23:58:58 +02:00
renovate[bot]
d48a4ab00a Lock file maintenance (#16231)
* Lock file maintenance

* bump @codemirror/view and eslint-plugin-lit to remove duplicates

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-04-19 21:08:48 +00:00
Paul Bottein
d89ac0f30d Add language selector (#16242) 2023-04-19 21:21:27 +02:00
Paul Bottein
3cb3f8d352 Add aliases description in voice dialog (#16233)
* Add aliases description in voice dialog

* Update style

* Remove duplicate margin
2023-04-19 13:52:55 +00:00
Bram Kragten
f507a7b8b3 Use yaml to show raw pipeline debug data (#16234) 2023-04-19 15:27:01 +02:00
Paul Bottein
910244f751 Fix sidebar tooltip (#16238) 2023-04-19 14:23:04 +02:00
Bram Kragten
a998465600 Fix demo deployment (#16178
Update demo_deployment.yaml
2023-04-13 21:37:03 +02:00
Bram Kragten
21645c2361 Bumped version to 20230411.1 2023-04-13 11:01:42 +02:00
Bram Kragten
e781be885d Fix context provider not updated on initializing of hass object (#16159) 2023-04-13 11:01:37 +02:00
akston
b15b0e25d6 Fix cropped person image transparency (#16112) 2023-04-13 11:01:18 +02:00
Paul Bottein
2d74873db0 Prevent edit and move view button triggered in the same time (#16143) 2023-04-13 11:00:57 +02:00
Bram Kragten
d50f2bf38c 20230411.0 (#16136) 2023-04-11 10:53:02 +02:00
123 changed files with 5783 additions and 3464 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

@@ -75,7 +75,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2022.10.1
uses: home-assistant/wheels@2023.04.0
with:
abi: cp310
tag: musllinux_1_2

View File

@@ -84,17 +84,23 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
babelrc: false,
compact: false,
assumptions: {
privateFieldsAsProperties: true,
setPublicClassFields: true,
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : "legacy",
presets: [
!latestBuild && [
[
"@babel/preset-env",
{
useBuiltIns: "entry",
corejs: { version: "3.30", proposals: true },
useBuiltIns: latestBuild ? false : "entry",
corejs: latestBuild ? false : { version: "3.30", proposals: true },
bugfixes: true,
},
],
"@babel/preset-typescript",
].filter(Boolean),
],
plugins: [
[
path.resolve(
@@ -106,22 +112,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
ignoreModuleNotFound: true,
},
],
// Part of ES2018. Converts {...a, b: 2} to Object.assign({}, a, {b: 2})
!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",
// Support some proposals still in TC39 process
["@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
isProdBuild && [
"template-html-minifier",

View File

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

View File

@@ -19,7 +19,7 @@ gulp.task(
"translations-enable-merge-backend",
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-cast",
"gen-index-cast-dev",
"gen-pages-cast-dev",
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"),
"copy-static-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",
gulp.parallel(
"gen-icons-json",
"gen-index-demo-dev",
"gen-pages-demo-dev",
"build-translations",
"build-locale-data"
),
@@ -42,6 +42,6 @@ gulp.task(
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
"copy-static-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 { htmlMinifierOptions, terserOptions } = require("../bundle.cjs");
const templatePath = (tpl) =>
path.resolve(paths.polymer_dir, "src/html/", `${tpl}.html.template`);
const readFile = (pth) => fs.readFileSync(pth).toString();
const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
const compiled = template(readFile(pathFunc(pth)));
const renderTemplate = (templateFile, data = {}) => {
const compiled = template(
fs.readFileSync(templateFile, { encoding: "utf-8" })
);
return compiled({
...data,
useRollup: env.useRollup(),
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 = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(paths.demo_dir, "src/html/", `${tpl}.html.template`)
);
const WRAP_TAGS = { ".js": "script", ".css": "style" };
const renderCastTemplate = (pth, data = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(paths.cast_dir, "src/html/", `${tpl}.html.template`)
);
const renderGalleryTemplate = (pth, data = {}) =>
renderTemplate(pth, data, (tpl) =>
path.resolve(paths.gallery_dir, "src/html/", `${tpl}.html.template`)
);
const minifyHtml = (content) =>
minify(content, {
const minifyHtml = (content, ext) => {
const wrapTag = WRAP_TAGS[ext] || "";
const begTag = wrapTag && `<${wrapTag}>`;
const endTag = wrapTag && `</${wrapTag}>`;
return minify(begTag + content + endTag, {
...htmlMinifierOptions,
conservativeCollapse: false,
minifyJS: terserOptions({
latestBuild: false, // Shared scripts should be ES5
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) => {
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: `/frontend_latest/${page}.js`,
es5PageJS: `/frontend_es5/${page}.js`,
});
fs.outputFileSync(
path.resolve(paths.app_output_root, `${page}.html`),
content
);
}
done();
});
gulp.task("gen-pages-prod", async () => {
const latestManifest = require(path.resolve(
paths.app_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.app_output_es5,
"manifest.json"
));
const minifiedHTML = [];
for (const page of PAGES) {
const content = renderTemplate(page, {
latestPageJS: latestManifest[`${page}.js`],
es5PageJS: es5Manifest[`${page}.js`],
});
minifiedHTML.push(
minifyHtml(content).then((minified) =>
fs.outputFileSync(
path.resolve(paths.app_output_root, `${page}.html`),
minified
// Same as previous but for production builds
// (includes minification and hashed file names from manifest)
const genPagesProdTask =
(
pageEntries,
inputRoot,
outputRoot,
outputLatest,
outputES5,
inputSub = "src/html"
) =>
async () => {
const latestManifest = require(path.resolve(outputLatest, "manifest.json"));
const es5Manifest = outputES5
? require(path.resolve(outputES5, "manifest.json"))
: {};
const minifiedHTML = [];
for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate(
path.resolve(inputRoot, inputSub, `${page}.template`),
{
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
latestCustomPanelJS: latestManifest["custom-panel.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"],
}
);
minifiedHTML.push(
minifyHtml(content, path.extname(page)).then((minified) =>
fs.outputFileSync(path.resolve(outputRoot, page), minified)
)
)
);
}
await Promise.all(minifiedHTML);
});
);
}
await Promise.all(minifiedHTML);
};
gulp.task("gen-index-app-dev", (done) => {
let latestAppJS;
let latestCoreJS;
let latestCustomPanelJS;
// Map HTML pages to their required entrypoints
const APP_PAGE_ENTRIES = {
"authorize.html": ["authorize"],
"onboarding.html": ["onboarding"],
"index.html": ["core", "app"],
};
if (env.useWDS()) {
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
latestCustomPanelJS =
"http://localhost:8000/src/entrypoints/custom-panel.ts";
} else {
latestAppJS = "/frontend_latest/app.js";
latestCoreJS = "/frontend_latest/core.js";
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
}
gulp.task(
"gen-pages-app-dev",
genPagesDevTask(
APP_PAGE_ENTRIES,
paths.polymer_dir,
paths.app_output_root,
env.useWDS()
)
);
const content = renderTemplate("index", {
latestAppJS,
latestCoreJS,
latestCustomPanelJS,
es5AppJS: "/frontend_es5/app.js",
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(
gulp.task(
"gen-pages-app-prod",
genPagesProdTask(
APP_PAGE_ENTRIES,
paths.polymer_dir,
paths.app_output_root,
paths.app_output_latest,
"manifest.json"
));
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"],
paths.app_output_es5
)
);
es5AppJS: es5Manifest["app.js"],
es5CoreJS: es5Manifest["core.js"],
es5CustomPanelJS: es5Manifest["custom-panel.js"],
});
const minified = (await minifyHtml(content)).replace(
/#THEMEC/g,
"{{ theme_color }}"
);
const CAST_PAGE_ENTRIES = {
"faq.html": ["launcher"],
"index.html": ["launcher"],
"media.html": ["media"],
"receiver.html": ["receiver"],
};
fs.outputFileSync(
path.resolve(paths.app_output_root, "index.html"),
minified
);
});
gulp.task(
"gen-pages-cast-dev",
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
);
gulp.task("gen-index-cast-dev", (done) => {
const contentReceiver = renderCastTemplate("receiver", {
latestReceiverJS: "/frontend_latest/receiver.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "receiver.html"),
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(
gulp.task(
"gen-pages-cast-prod",
genPagesProdTask(
CAST_PAGE_ENTRIES,
paths.cast_dir,
paths.cast_output_root,
paths.cast_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.cast_output_es5,
"manifest.json"
));
paths.cast_output_es5
)
);
const contentReceiver = renderCastTemplate("receiver", {
latestReceiverJS: latestManifest["receiver.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "receiver.html"),
contentReceiver
);
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
const contentMedia = renderCastTemplate("media", {
latestMediaJS: latestManifest["media.js"],
es5MediaJS: es5Manifest["media.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
gulp.task(
"gen-pages-demo-dev",
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
);
const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: latestManifest["launcher.js"],
es5LauncherJS: es5Manifest["launcher.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "faq.html"),
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(
gulp.task(
"gen-pages-demo-prod",
genPagesProdTask(
DEMO_PAGE_ENTRIES,
paths.demo_dir,
paths.demo_output_root,
paths.demo_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.demo_output_es5,
"manifest.json"
));
const content = renderDemoTemplate("index", {
latestDemoJS: latestManifest["main.js"],
paths.demo_output_es5
)
);
es5DemoJS: es5Manifest["main.js"],
});
const minified = await minifyHtml(content);
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
fs.outputFileSync(
path.resolve(paths.demo_output_root, "index.html"),
minified
);
});
gulp.task(
"gen-pages-gallery-dev",
genPagesDevTask(
GALLERY_PAGE_ENTRIES,
paths.gallery_dir,
paths.gallery_output_root
)
);
gulp.task("gen-index-gallery-dev", (done) => {
const content = renderGalleryTemplate("index", {
latestGalleryJS: "./frontend_latest/entrypoint.js",
});
gulp.task(
"gen-pages-gallery-prod",
genPagesProdTask(
GALLERY_PAGE_ENTRIES,
paths.gallery_dir,
paths.gallery_output_root,
paths.gallery_output_latest
)
);
fs.outputFileSync(
path.resolve(paths.gallery_output_root, "index.html"),
content
);
done();
});
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
gulp.task("gen-index-gallery-prod", async () => {
const latestManifest = require(path.resolve(
paths.gallery_output_latest,
"manifest.json"
));
const content = renderGalleryTemplate("index", {
latestGalleryJS: latestManifest["entrypoint.js"],
});
const minified = await minifyHtml(content);
gulp.task(
"gen-pages-hassio-dev",
genPagesDevTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
undefined,
"src",
paths.hassio_publicPath
)
);
fs.outputFileSync(
path.resolve(paths.gallery_output_root, "index.html"),
minified
);
});
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(
gulp.task(
"gen-pages-hassio-prod",
genPagesProdTask(
HASSIO_PAGE_ENTRIES,
paths.hassio_dir,
paths.hassio_output_root,
paths.hassio_output_latest,
"manifest.json"
));
const es5Manifest = require(path.resolve(
paths.hassio_output_es5,
"manifest.json"
));
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" }
);
}
"src"
)
);

View File

@@ -159,7 +159,7 @@ gulp.task(
"gather-gallery-pages"
),
"copy-static-gallery",
"gen-index-gallery-dev",
"gen-pages-gallery-dev",
gulp.parallel(
env.useRollup()
? "rollup-dev-server-gallery"
@@ -193,6 +193,6 @@ gulp.task(
),
"copy-static-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",
"gen-dummy-icons-json",
"gen-index-hassio-dev",
"gen-pages-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
"build-locale-data",
@@ -39,7 +39,7 @@ gulp.task(
"build-locale-data",
"copy-locale-data-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
"gen-pages-hassio-prod",
...// Don't compress running tests
(env.isTestBuild() ? [] : ["compress-hassio"])
)

View File

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

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
// Script to print Babel plugins and Core JS polyfills that will be used by browserslist environments
import { version as babelVersion } from "@babel/core";
import presetEnv from "@babel/preset-env";
import compilationTargets from "@babel/helper-compilation-targets";
import coreJSCompat from "core-js-compat";
import { logPlugin } from "@babel/preset-env/lib/debug.js";
import { babelOptions } from "./bundle.cjs";
const detailsOpen = (heading) =>
`<details>\n<summary><h4>${heading}</h4></summary>\n`;
const detailsClose = "</details>\n";
const dummyAPI = {
version: babelVersion,
assertVersion: () => {},
caller: (callback) =>
callback({
name: "Dummy Bundler",
supportsStaticESM: true,
supportsDynamicImport: true,
supportsTopLevelAwait: true,
supportsExportNamespaceFrom: true,
}),
targets: () => ({}),
};
for (const buildType of ["Modern", "Legacy"]) {
const browserslistEnv = buildType.toLowerCase();
const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" });
const presetEnvOpts = babelOpts.presets[0][1];
// Invoking preset-env in debug mode will log the included plugins
console.log(detailsOpen(`${buildType} Build Babel Plugins`));
presetEnv.default(dummyAPI, {
...presetEnvOpts,
browserslistEnv,
debug: true,
});
console.log(detailsClose);
// Manually log the Core-JS polyfills using the same technique
if (presetEnvOpts.useBuiltIns) {
console.log(detailsOpen(`${buildType} Build Core-JS Polyfills`));
const targets = compilationTargets.default(babelOpts?.targets, {
browserslistEnv,
});
const polyfillList = coreJSCompat({ targets }).list;
console.log(
"The following %i polyfills may be injected by Babel:\n",
polyfillList.length
);
for (const polyfill of polyfillList) {
logPlugin(polyfill, targets, coreJSCompat.data);
}
console.log(detailsClose);
}
}

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

View File

@@ -1,8 +1,10 @@
<!DOCTYPE html>
<html>
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<script type="module" src="<%= latestReceiverJS %>"></script>
<%= renderTemplate('_style_base') %>
<% for (const entry of latestEntryJS) { %>
<script type="module" src="<%= entry %>"></script>
<% } %>
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
<style>
body {
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>
<html>
<head>
<meta charset="utf-8" />
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" href="/static/icons/favicon.ico" />
<title>Home Assistant Demo</title>
<%= renderTemplate("../../../src/html/_header.html.template") %>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link
rel="apple-touch-icon"
@@ -35,33 +34,7 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#03a9f4" />
<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"
/>
<title>Home Assistant Demo</title>
<%= renderTemplate("_social_meta.html.template") %>
<style>
html {
background-color: var(--primary-background-color, #fafafa);
@@ -107,29 +80,22 @@
</svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
</div>
<ha-demo></ha-demo>
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<script>
import("<%= latestDemoJS %>");
window.latestJS = true;
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5DemoJS %>");
};
<% } else { %>
_ls("<%= es5DemoJS %>");
if (!window.globalThis) {
window.globalThis = window;
}
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
}
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {

View File

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

View File

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

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

@@ -27,19 +27,20 @@
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@codemirror/autocomplete": "6.5.1",
"@codemirror/commands": "6.2.2",
"@codemirror/commands": "6.2.3",
"@codemirror/language": "6.6.0",
"@codemirror/legacy-modes": "6.3.2",
"@codemirror/search": "6.3.0",
"@codemirror/state": "6.2.0",
"@codemirror/view": "6.9.4",
"@codemirror/view": "6.9.5",
"@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-locale": "3.1.1",
"@formatjs/intl-numberformat": "8.3.5",
"@formatjs/intl-pluralrules": "5.1.10",
"@formatjs/intl-relativetimeformat": "11.1.10",
"@formatjs/intl-locale": "3.2.1",
"@formatjs/intl-numberformat": "8.4.1",
"@formatjs/intl-pluralrules": "5.2.1",
"@formatjs/intl-relativetimeformat": "11.2.1",
"@fullcalendar/core": "6.1.5",
"@fullcalendar/daygrid": "6.1.5",
"@fullcalendar/interaction": "6.1.5",
@@ -90,8 +91,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "23.3.10",
"@vaadin/vaadin-themable-mixin": "23.3.10",
"@vaadin/combo-box": "23.3.11",
"@vaadin/vaadin-themable-mixin": "23.3.11",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -108,11 +109,11 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"fuse.js": "6.6.2",
"google-timezones-json": "1.0.2",
"google-timezones-json": "1.1.0",
"hls.js": "1.3.5",
"home-assistant-js-websocket": "8.0.1",
"idb-keyval": "6.2.0",
"intl-messageformat": "10.3.3",
"intl-messageformat": "10.3.4",
"js-yaml": "4.1.0",
"leaflet": "1.9.3",
"leaflet-draw": "1.0.4",
@@ -149,15 +150,7 @@
},
"devDependencies": {
"@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-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-typescript": "7.21.4",
"@koa/cors": "4.0.0",
@@ -185,8 +178,8 @@
"@types/sortablejs": "1.15.1",
"@types/tar": "6.1.4",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.2",
@@ -200,14 +193,14 @@
"eslint-import-resolver-webpack": "0.13.2",
"eslint-plugin-disable": "2.0.3",
"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-unused-imports": "2.0.0",
"eslint-plugin-wc": "1.4.0",
"esprima": "4.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.1.1",
"glob": "10.1.0",
"glob": "10.2.1",
"gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.4.8",
@@ -234,7 +227,7 @@
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.9.0",
"serve-handler": "6.1.5",
"sinon": "15.0.3",
"sinon": "15.0.4",
"source-map-url": "0.4.1",
"systemjs": "6.14.1",
"tar": "6.1.13",

View File

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

View File

@@ -50,6 +50,7 @@ import {
mdiRobotVacuum,
mdiScriptText,
mdiSineWave,
mdiSpeakerMessage,
mdiSpeedometer,
mdiSunWireless,
mdiThermometer,
@@ -75,8 +76,8 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */
export const FIXED_DOMAIN_ICONS = {
alert: mdiAlert,
air_quality: mdiAirFilter,
alert: mdiAlert,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
@@ -106,10 +107,12 @@ export const FIXED_DOMAIN_ICONS = {
script: mdiScriptText,
select: mdiFormatListBulleted,
sensor: mdiEye,
siren: mdiBullhorn,
simple_alarm: mdiBell,
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
text: mdiFormTextbox,
timer: mdiTimerOutline,
tts: mdiSpeakerMessage,
updater: mdiCloudUpload,
vacuum: mdiRobotVacuum,
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 (
domain === "button" ||
domain === "input_button" ||
domain === "scene" ||
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
(domain === "sensor" && attributes.device_class === "timestamp")
) {
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 shouldPolyfillRelativeTime } from "@formatjs/intl-relativetimeformat/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 { Resources, TranslationDict } from "../../types";
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/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 =
@@ -216,6 +221,17 @@ export const loadPolyfillLocales = async (language: string) => {
// @ts-ignore
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) {
// Ignore
}

View File

@@ -666,6 +666,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell.mdc-data-table__cell--flex {
display: flex;
overflow: initial;
}
.mdc-data-table__cell.mdc-data-table__cell--icon {

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;
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 { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
registerStyles(

View File

@@ -4,11 +4,12 @@ import {
html,
LitElement,
nothing,
PropertyValueMap,
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";
@@ -16,10 +17,13 @@ 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;
@@ -30,13 +34,19 @@ export class HaConversationAgentPicker extends LitElement {
@state() _agents?: Agent[];
@state() _defaultAgent: string | null = null;
protected render() {
if (!this._agents) {
return nothing;
}
const value = this.value ?? (this.required ? this._defaultAgent : NONE);
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 ||
@@ -60,20 +70,56 @@ export class HaConversationAgentPicker extends LitElement {
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item .value=${agent.id}>${agent.name}</ha-list-item>`
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}
</ha-select>
`;
}
protected firstUpdated(
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.firstUpdated(changedProperties);
listAgents(this.hass).then((agents) => {
this._agents = agents.agents;
this._defaultAgent = agents.default_agent;
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 {
@@ -96,6 +142,10 @@ export class HaConversationAgentPicker extends LitElement {
}
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,
});
}
}
@@ -103,4 +153,7 @@ 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 "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button"];
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = (
hass: HomeAssistant,
@@ -92,7 +92,7 @@ export class HaDialog extends DialogBase {
padding: 24px 24px 0 24px;
}
.mdc-dialog__actions {
padding: 0 24px 24px 24px;
padding: 12px 24px 12px 24px;
}
.mdc-dialog__title::before {
display: block;

View File

@@ -65,6 +65,7 @@ export class HaDrawer extends DrawerBase {
.mdc-drawer {
position: fixed;
top: 0;
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;

View File

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

View File

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

View File

@@ -211,6 +211,7 @@ export class Gauge extends LitElement {
font-size: 50px;
fill: var(--primary-text-color);
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

@@ -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

@@ -20,10 +20,16 @@ export class HaConversationAgentSelector extends LitElement {
@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}

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

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

@@ -14,6 +14,7 @@ const LOAD_ELEMENTS = {
addon: () => import("./ha-selector-addon"),
area: () => import("./ha-selector-area"),
attribute: () => import("./ha-selector-attribute"),
assist_pipeline: () => import("./ha-selector-assist-pipeline"),
boolean: () => import("./ha-selector-boolean"),
color_rgb: () => import("./ha-selector-color-rgb"),
config_entry: () => import("./ha-selector-config-entry"),
@@ -26,6 +27,7 @@ const LOAD_ELEMENTS = {
entity: () => import("./ha-selector-entity"),
statistic: () => import("./ha-selector-statistic"),
file: () => import("./ha-selector-file"),
language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"),
object: () => import("./ha-selector-object"),
@@ -40,6 +42,7 @@ const LOAD_ELEMENTS = {
media: () => import("./ha-selector-media"),
theme: () => import("./ha-selector-theme"),
tts: () => import("./ha-selector-tts"),
tts_voice: () => import("./ha-selector-tts-voice"),
location: () => import("./ha-selector-location"),
color_temp: () => import("./ha-selector-color-temp"),
ui_action: () => import("./ha-selector-ui-action"),
@@ -72,8 +75,9 @@ export class HaSelector extends LitElement {
@property() public context?: Record<string, any>;
public focus() {
this.shadowRoot?.getElementById("selector")?.focus();
public async focus() {
await this.updateComplete;
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
}
private get _type() {

View File

@@ -52,18 +52,17 @@ export class HaSettingsRow extends LitElement {
white-space: nowrap;
}
.body > .secondary {
font-family: var(--paper-font-body1_-_font-family);
-webkit-font-smoothing: var(
--paper-font-body1_-_-webkit-font-smoothing
);
font-size: var(--paper-font-body1_-_font-size);
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)
display: block;
padding-top: 4px;
font-family: var(
--mdc-typography-body2-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
-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] {
min-height: calc(

View File

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

View File

@@ -5,7 +5,6 @@ import {
LitElement,
nothing,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@@ -20,6 +19,8 @@ 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;
@@ -34,13 +35,18 @@ export class HaSTTPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state() _engines: STTEngine[] = [];
@state() _engines?: STTEngine[];
protected render(): TemplateResult {
protected render() {
if (!this._engines) {
return nothing;
}
const value =
this.value ??
(this.required
? this._engines.find((engine) => engine.language_supported)
? this._engines.find(
(engine) => engine.supported_languages?.length !== 0
)
: NONE);
return html`
<ha-select
@@ -60,12 +66,18 @@ export class HaSTTPicker extends LitElement {
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
const stateObj = this.hass!.states[engine.engine_id];
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.language_supported === false}
.disabled=${engine.supported_languages?.length === 0}
>
${stateObj ? computeStateName(stateObj) : engine.engine_id}
${label}
</ha-list-item>`;
})}
</ha-select>
@@ -84,13 +96,27 @@ export class HaSTTPicker extends LitElement {
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (await listSTTEngines(this.hass, this.language)).providers;
this._engines = (
await listSTTEngines(
this.hass,
this.language,
this.hass.config.country || undefined
)
).providers;
if (
this.value &&
!this._engines.find((engine) => engine.engine_id === this.value)
?.language_supported
) {
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 });
}
@@ -116,6 +142,10 @@ export class HaSTTPicker extends LitElement {
}
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,
});
}
}

View File

@@ -1,4 +1,3 @@
import { debounce } from "chart.js/helpers";
import {
css,
CSSResultGroup,
@@ -6,12 +5,12 @@ import {
LitElement,
nothing,
PropertyValues,
TemplateResult,
} 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";
@@ -20,6 +19,8 @@ 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;
@@ -34,13 +35,18 @@ export class HaTTSPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state() _engines: TTSEngine[] = [];
@state() _engines?: TTSEngine[];
protected render(): TemplateResult {
protected render() {
if (!this._engines) {
return nothing;
}
const value =
this.value ??
(this.required
? this._engines.find((engine) => engine.language_supported)
? this._engines.find(
(engine) => engine.supported_languages?.length !== 0
)
: NONE);
return html`
<ha-select
@@ -60,12 +66,18 @@ export class HaTTSPicker extends LitElement {
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
const stateObj = this.hass!.states[engine.engine_id];
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.language_supported === false}
.disabled=${engine.supported_languages?.length === 0}
>
${stateObj ? computeStateName(stateObj) : engine.engine_id}
${label}
</ha-list-item>`;
})}
</ha-select>
@@ -84,13 +96,27 @@ export class HaTTSPicker extends LitElement {
private _debouncedUpdateEngines = debounce(() => this._updateEngines(), 500);
private async _updateEngines() {
this._engines = (await listTTSEngines(this.hass, this.language)).providers;
this._engines = (
await listTTSEngines(
this.hass,
this.language,
this.hass.config.country || undefined
)
).providers;
if (
this.value &&
!this._engines.find((engine) => engine.engine_id === this.value)
?.language_supported
) {
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 });
}
@@ -116,6 +142,10 @@ export class HaTTSPicker extends LitElement {
}
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,
});
}
}

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 { html, LitElement, nothing } from "lit";
import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
@@ -31,6 +31,8 @@ export class HaYamlEditor extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public autoUpdate = false;
@property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public required = false;
@@ -41,7 +43,11 @@ export class HaYamlEditor extends LitElement {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, { schema: this.yamlSchema, quotingType: '"' })
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
} catch (err: any) {
// 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() {
if (this._yaml === undefined) {
return nothing;

View File

@@ -9,5 +9,11 @@ export interface AlexaEntity {
export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<AlexaEntity>({
type: "cloud/alexa/entities/get",
entity_id,
});
export const syncCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS({ type: "cloud/alexa/sync" });

View File

@@ -5,19 +5,27 @@ import type { SpeechMetadata } from "./stt";
export interface AssistPipeline {
id: string;
conversation_engine: string;
language: string;
name: string;
stt_engine: string;
tts_engine: 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 {
conversation_engine: string;
language: string;
name: string;
stt_engine: string;
tts_engine: 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 {
@@ -71,6 +79,7 @@ interface PipelineIntentStartEvent extends PipelineEventBase {
type: "intent-start";
data: {
engine: string;
language: string;
intent_input: string;
};
}
@@ -85,6 +94,8 @@ interface PipelineTTSStartEvent extends PipelineEventBase {
type: "tts-start";
data: {
engine: string;
language: string;
voice: string;
tts_input: string;
};
}
@@ -202,14 +213,15 @@ export const processEvent = (
return run;
};
export const runAssistPipeline = (
export const runDebugAssistPipeline = (
hass: HomeAssistant,
callback: (event: PipelineRun) => void,
callback: (run: PipelineRun) => void,
options: PipelineRunOptions
) => {
let run: PipelineRun | undefined;
const unsubProm = hass.connection.subscribeMessage<PipelineRunEvent>(
const unsubProm = runAssistPipeline(
hass,
(updateEvent) => {
run = processEvent(run, updateEvent, options);
@@ -221,15 +233,22 @@ export const runAssistPipeline = (
callback(run);
}
},
{
...options,
type: "assist_pipeline/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
@@ -254,7 +273,7 @@ export const getAssistPipelineRun = (
pipeline_run_id,
});
export const fetchAssistPipelines = (hass: HomeAssistant) =>
export const listAssistPipelines = (hass: HomeAssistant) =>
hass.callWS<{
pipelines: AssistPipeline[];
preferred_pipeline: string | null;
@@ -262,6 +281,12 @@ export const fetchAssistPipelines = (hass: HomeAssistant) =>
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
@@ -274,7 +299,7 @@ export const createAssistPipeline = (
export const updateAssistPipeline = (
hass: HomeAssistant,
pipeline_id: string,
pipeline: Partial<AssistPipelineMutableParams>
pipeline: AssistPipelineMutableParams
) =>
hass.callWS<AssistPipeline>({
type: "assist_pipeline/pipeline/update",
@@ -296,3 +321,8 @@ export const deleteAssistPipeline = (hass: HomeAssistant, pipelineId: string) =>
type: "assist_pipeline/pipeline/delete",
pipeline_id: pipelineId,
});
export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
hass.callWS<{ languages: string[] }>({
type: "assist_pipeline/language/list",
});

View File

@@ -76,10 +76,14 @@ export const cloudLogin = (
email: string,
password: string
) =>
hass.callApi("POST", "cloud/login", {
email,
password,
});
hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
"POST",
"cloud/login",
{
email,
password,
}
);
export const cloudLogout = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/logout");

View File

@@ -59,6 +59,7 @@ export interface AgentInfo {
export interface Agent {
id: string;
name: string;
supported_languages: "*" | string[];
}
export const processConversationInput = (
@@ -76,10 +77,14 @@ export const processConversationInput = (
});
export const listAgents = (
hass: HomeAssistant
): Promise<{ agents: Agent[]; default_agent: string | null }> =>
hass: HomeAssistant,
language?: string,
country?: string
): Promise<{ agents: Agent[] }> =>
hass.callWS({
type: "conversation/agent/list",
language,
country,
});
export const getAgentInfo = (

View File

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

View File

@@ -26,11 +26,13 @@ export type Selector =
| LegacyEntitySelector
| FileSelector
| IconSelector
| LanguageSelector
| LocationSelector
| MediaSelector
| NavigationSelector
| NumberSelector
| ObjectSelector
| AssistPipelineSelector
| SelectSelector
| StateSelector
| StatisticSelector
@@ -41,6 +43,7 @@ export type Selector =
| ThemeSelector
| TimeSelector
| TTSSelector
| TTSVoiceSelector
| UiActionSelector
| UiColorSelector;
@@ -89,8 +92,7 @@ export interface ColorTempSelector {
}
export interface ConversationAgentSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
conversation_agent: {} | null;
conversation_agent: { language?: string } | null;
}
export interface ConfigEntrySelector {
@@ -209,6 +211,14 @@ export interface IconSelector {
} | null;
}
export interface LanguageSelector {
language: {
languages?: string[];
native_name?: boolean;
no_sort?: boolean;
} | null;
}
export interface LocationSelector {
location: { radius?: boolean; icon?: string } | null;
}
@@ -257,6 +267,11 @@ export interface ObjectSelector {
object: {} | null;
}
export interface AssistPipelineSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
assist_pipeline: {} | null;
}
export interface SelectOption {
value: any;
label: string;
@@ -331,6 +346,10 @@ export interface TTSSelector {
tts: { language?: string } | null;
}
export interface TTSVoiceSelector {
tts_voice: { engineId?: string; language?: string } | null;
}
export interface UiActionSelector {
ui_action: {
actions?: UiAction[];

View File

@@ -20,14 +20,16 @@ export interface SpeechMetadata {
export interface STTEngine {
engine_id: string;
language_supported?: boolean;
supported_languages?: string[];
}
export const listSTTEngines = (
hass: HomeAssistant,
language?: string
language?: string,
country?: string
): Promise<{ providers: STTEngine[] }> =>
hass.callWS({
type: "stt/engine/list",
language,
country,
});

View File

@@ -2,7 +2,7 @@ import { HomeAssistant } from "../types";
export interface TTSEngine {
engine_id: string;
language_supported?: boolean;
supported_languages?: string[];
}
export interface TTSVoice {
@@ -31,18 +31,20 @@ export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
export const listTTSEngines = (
hass: HomeAssistant,
language?: string
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[] }> =>
): Promise<{ voices: TTSVoice[] | null }> =>
hass.callWS({
type: "tts/engine/voices",
engine_id,

View File

@@ -12,8 +12,6 @@ export const voiceAssistants = {
},
} as const;
export const voiceAssistantKeys = Object.keys(voiceAssistants);
export const setExposeNewEntities = (
hass: HomeAssistant,
assistant: string,

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,7 +108,6 @@ class DialogRestart extends LitElement {
graphic="avatar"
twoline
multiline-secondary
hasMeta
@request-selected=${this._reload}
>
<div slot="graphic" class="icon-background reload">
@@ -131,7 +130,6 @@ class DialogRestart extends LitElement {
graphic="avatar"
twoline
multiline-secondary
hasMeta
@request-selected=${this._restart}
>
<div slot="graphic" class="icon-background restart">

View File

@@ -0,0 +1,187 @@
import { mdiPlayCircleOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea";
import { convertTextToSpeech } from "../../data/tts";
import { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/ha-circular-progress";
@customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loadingExample = false;
@state() private _params?: TTSTryDialogParams;
@state() private _valid = false;
@query("#message") private _messageInput?: HaTextArea;
@LocalStorage("ttsTryMessages", false, false) private _messages?: Record<
string,
string
>;
public showDialog(params: TTSTryDialogParams) {
this._params = params;
this._valid = Boolean(this._defaultMessage);
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private get _defaultMessage() {
const language = this._params!.language?.substring(0, 2);
const userLanguage = this.hass.locale.language.substring(0, 2);
// Load previous message in the right language
if (language && this._messages?.[language]) {
return this._messages[language];
}
// Only display example message if it's interface language
if (language === userLanguage) {
return this.hass.localize("ui.dialogs.tts-try.message_example");
}
return "";
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.tts-try.header")
)}
>
<ha-textarea
autogrow
id="message"
.label=${this.hass.localize("ui.dialogs.tts-try.message")}
.placeholder=${this.hass.localize(
"ui.dialogs.tts-try.message_placeholder"
)}
.value=${this._defaultMessage}
@input=${this._inputChanged}
?dialogInitialFocus=${!this._defaultMessage}
>
</ha-textarea>
${this._loadingExample
? html`
<ha-circular-progress
size="small"
active
alt=""
slot="primaryAction"
class="loading"
></ha-circular-progress>
`
: html`
<ha-button
?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample}
.disabled=${!this._valid}
>
<ha-svg-icon
slot="icon"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</ha-button>
`}
</ha-dialog>
`;
}
private async _inputChanged() {
this._valid = Boolean(this._messageInput?.value);
}
private async _playExample() {
const message = this._messageInput?.value;
if (!message) {
return;
}
const platform = this._params!.engine;
const language = this._params!.language;
const voice = this._params!.voice;
if (language) {
this._messages = {
...this._messages,
[language.substring(0, 2)]: message,
};
}
this._loadingExample = true;
const audio = new Audio();
audio.play();
let url;
try {
const result = await convertTextToSpeech(this.hass, {
platform,
message,
language,
options: { voice },
});
url = result.path;
} catch (err: any) {
this._loadingExample = false;
showAlertDialog(this, {
text: `Unable to load example. ${err.error || err.body || err}`,
warning: true,
});
return;
}
audio.src = url;
audio.addEventListener("canplaythrough", () => audio.play());
audio.addEventListener("playing", () => {
this._loadingExample = false;
});
audio.addEventListener("error", () => {
showAlertDialog(this, { title: "Error playing audio." });
this._loadingExample = false;
});
}
static get styles(): CSSResultGroup {
return css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-textarea,
ha-select {
width: 100%;
}
ha-select {
margin-top: 8px;
}
.loading {
height: 36px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-tts-try": TTSTryDialog;
}
}

View File

@@ -0,0 +1,21 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface TTSTryDialogParams {
engine: string;
language?: string;
voice?: string;
}
export const loadTTSTryDialog = () => import("./dialog-tts-try");
export const showTTSTryDialog = (
element: HTMLElement,
dialogParams: TTSTryDialogParams
): void => {
fireEvent(element, "show-dialog", {
addHistory: false,
dialogTag: "dialog-tts-try",
dialogImport: loadTTSTryDialog,
dialogParams,
});
};

View File

@@ -1,38 +1,44 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button/mwc-button";
import {
mdiChevronDown,
mdiClose,
mdiHelpCircleOutline,
mdiMicrophone,
mdiSend,
mdiStar,
} from "@mdi/js";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { LocalStorage } from "../../common/decorators/local-storage";
import { fireEvent } from "../../common/dom/fire_event";
import { SpeechRecognition } from "../../common/dom/speech-recognition";
import { stopPropagation } from "../../common/dom/stop_propagation";
import "../../components/ha-button";
import "../../components/ha-button-menu";
import "../../components/ha-dialog";
import type { HaDialog } from "../../components/ha-dialog";
import "../../components/ha-header-bar";
import "../../components/ha-icon-button";
import "../../components/ha-list-item";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import {
AgentInfo,
getAgentInfo,
prepareConversation,
processConversationInput,
} from "../../data/conversation";
AssistPipeline,
getAssistPipeline,
listAssistPipelines,
runAssistPipeline,
} from "../../data/assist_pipeline";
import { AgentInfo, getAgentInfo } from "../../data/conversation";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { AudioRecorder } from "../../util/audio-recorder";
import { documentationUrl } from "../../util/documentation-url";
import { showAlertDialog } from "../generic/show-dialog-box";
interface Message {
who: string;
@@ -40,49 +46,62 @@ interface Message {
error?: boolean;
}
interface Results {
transcript: string;
final: boolean;
}
@customElement("ha-voice-command-dialog")
export class HaVoiceCommandDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public results: Results | null = null;
@state() private _conversation: Message[] = [
{
who: "hass",
text: "",
},
];
@state() private _conversation?: Message[];
@state() private _opened = false;
@LocalStorage("AssistPipelineId", true, false) private _pipelineId?: string;
@state() private _pipeline?: AssistPipeline;
@state() private _agentInfo?: AgentInfo;
@state() private _showSendButton = false;
@query("#scroll-container") private _scrollContainer!: HaDialog;
@state() private _pipelines?: AssistPipeline[];
@state() private _preferredPipeline?: string;
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
@query("#message-input") private _messageInput!: HaTextField;
private recognition!: SpeechRecognition;
private _conversationId: string | null = null;
private _audioRecorder?: AudioRecorder;
private _audioBuffer?: Int16Array[];
private _audio?: HTMLAudioElement;
private _stt_binary_handler_id?: number | null;
public async showDialog(): Promise<void> {
this._conversation = [
{
who: "hass",
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
},
];
this._opened = true;
this._agentInfo = await getAgentInfo(this.hass);
await this.updateComplete;
this._scrollMessagesBottom();
}
public async closeDialog(): Promise<void> {
this._opened = false;
if (this.recognition) {
this.recognition.abort();
}
this._pipeline = undefined;
this._pipelines = undefined;
this._agentInfo = undefined;
this._conversation = undefined;
this._conversationId = null;
this._audioRecorder?.close();
this._audioRecorder = undefined;
this._audio?.pause();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -90,6 +109,7 @@ export class HaVoiceCommandDialog extends LitElement {
if (!this._opened) {
return nothing;
}
const supportsSTT = this._pipeline?.stt_engine && AudioRecorder.isSupported;
return html`
<ha-dialog
open
@@ -99,15 +119,56 @@ export class HaVoiceCommandDialog extends LitElement {
>
<div slot="heading">
<ha-header-bar>
<span slot="title">
${this.hass.localize("ui.dialogs.voice_command.title")}
</span>
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<div slot="title">
${this.hass.localize("ui.dialogs.voice_command.title")}
<ha-button-menu
@opened=${this._loadPipelines}
@closed=${stopPropagation}
activatable
fixed
>
<ha-button slot="trigger">
${this._pipeline?.name}
<ha-svg-icon
slot="trailingIcon"
.path=${mdiChevronDown}
></ha-svg-icon>
</ha-button>
${this._pipelines?.map(
(pipeline) => html`<ha-list-item
?selected=${pipeline.id === this._pipelineId ||
(!this._pipelineId &&
pipeline.id === this._preferredPipeline)}
.pipeline=${pipeline.id}
@click=${this._selectPipeline}
.hasMeta=${pipeline.id === this._preferredPipeline}
>
${pipeline.name}${pipeline.id === this._preferredPipeline
? html`<ha-svg-icon
slot="meta"
.path=${mdiStar}
></ha-svg-icon>`
: nothing}
</ha-list-item>`
)}
${this.hass.user?.is_admin
? html`<li divider role="separator"></li>
<a href="/config/voice-assistants/assistants"
><ha-list-item @click=${this.closeDialog}
>${this.hass.localize(
"ui.dialogs.voice_command.manage_assistants"
)}</ha-list-item
></a
>`
: nothing}
</ha-button-menu>
</div>
<a
href=${documentationUrl(this.hass, "/docs/assist/")}
slot="actionItems"
@@ -123,25 +184,13 @@ export class HaVoiceCommandDialog extends LitElement {
</div>
<div class="messages">
<div class="messages-container" id="scroll-container">
${this._conversation.map(
${this._conversation!.map(
(message) => html`
<div class=${this._computeMessageClasses(message)}>
${message.text}
</div>
`
)}
${this.results
? html`
<div class="message user">
<span
class=${classMap({
interimTranscript: !this.results.final,
})}
>${this.results.transcript}</span
>${!this.results.final ? "…" : ""}
</div>
`
: ""}
</div>
</div>
<div class="input" slot="primaryAction">
@@ -166,9 +215,9 @@ export class HaVoiceCommandDialog extends LitElement {
>
</ha-icon-button>
`
: SpeechRecognition
: supportsSTT
? html`
${this.results
${this._audioRecorder?.active
? html`
<div class="bouncer">
<div class="double-bounce1"></div>
@@ -205,15 +254,51 @@ export class HaVoiceCommandDialog extends LitElement {
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
protected willUpdate(changedProperties: PropertyValues): void {
if (
changedProperties.has("_pipelineId") ||
(changedProperties.has("_opened") && this._opened === true)
) {
this._getPipeline();
}
}
private async _getPipeline() {
try {
this._pipeline = await getAssistPipeline(this.hass, this._pipelineId);
} catch (e: any) {
if (e.code === "not_found") {
this._pipelineId = undefined;
}
return;
}
this._agentInfo = await getAgentInfo(
this.hass,
this._pipeline.conversation_engine
);
}
private async _loadPipelines() {
if (this._pipelines) {
return;
}
const { pipelines, preferred_pipeline } = await listAssistPipelines(
this.hass
);
this._pipelines = pipelines;
this._preferredPipeline = preferred_pipeline || undefined;
}
private async _selectPipeline(ev: CustomEvent) {
this._pipelineId = (ev.currentTarget as any).pipeline;
this._conversation = [
{
who: "hass",
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
},
];
prepareConversation(this.hass, this.hass.language);
await this.updateComplete;
this._scrollMessagesBottom();
}
protected updated(changedProps: PropertyValues) {
@@ -224,7 +309,7 @@ export class HaVoiceCommandDialog extends LitElement {
}
private _addMessage(message: Message) {
this._conversation = [...this._conversation, message];
this._conversation = [...this._conversation!, message];
}
private _handleKeyUp(ev: KeyboardEvent) {
@@ -253,75 +338,8 @@ export class HaVoiceCommandDialog extends LitElement {
}
}
private _initRecognition() {
this.recognition = new SpeechRecognition();
this.recognition.interimResults = true;
this.recognition.lang = this.hass.language;
this.recognition.continuous = false;
this.recognition.addEventListener("start", () => {
this.results = {
final: false,
transcript: "",
};
});
this.recognition.addEventListener("nomatch", () => {
this._addMessage({
who: "user",
text: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_understand"
)}>`,
error: true,
});
});
this.recognition.addEventListener("error", (event) => {
// eslint-disable-next-line
console.error("Error recognizing text", event);
this.recognition!.abort();
// @ts-ignore
if (event.error !== "aborted" && event.error !== "no-speech") {
const text =
this.results && this.results.transcript
? this.results.transcript
: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_hear"
)}>`;
this._addMessage({ who: "user", text, error: true });
}
this.results = null;
});
this.recognition.addEventListener("end", () => {
// Already handled by onerror
if (this.results == null) {
return;
}
const text = this.results.transcript;
this.results = null;
if (text) {
this._processText(text);
} else {
this._addMessage({
who: "user",
text: `<${this.hass.localize(
"ui.dialogs.voice_command.did_not_hear"
)}>`,
error: true,
});
}
});
this.recognition.addEventListener("result", (event) => {
const result = event.results[0];
this.results = {
transcript: result[0].transcript,
final: result.isFinal,
};
});
}
private async _processText(text: string) {
if (this.recognition) {
this.recognition.abort();
}
this._audio?.pause();
this._addMessage({ who: "user", text });
const message: Message = {
who: "hass",
@@ -330,21 +348,33 @@ export class HaVoiceCommandDialog extends LitElement {
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(message);
try {
const response = await processConversationInput(
const unsub = await runAssistPipeline(
this.hass,
text,
this._conversationId,
this.hass.language
(event) => {
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
message.text = plain.speech;
}
this.requestUpdate("_conversation");
unsub();
}
if (event.type === "error") {
message.text = event.data.message;
message.error = true;
this.requestUpdate("_conversation");
unsub();
}
},
{
start_stage: "intent",
input: { text },
end_stage: "intent",
pipeline: this._pipelineId,
conversation_id: this._conversationId,
}
);
this._conversationId = response.conversation_id;
const plain = response.response.speech?.plain;
if (plain) {
message.text = plain.speech;
} else {
message.text = "<silence>";
}
this.requestUpdate("_conversation");
} catch {
message.text = this.hass.localize("ui.dialogs.voice_command.error");
message.error = true;
@@ -353,37 +383,174 @@ export class HaVoiceCommandDialog extends LitElement {
}
private _toggleListening() {
if (!this.results) {
if (!this._audioRecorder?.active) {
this._startListening();
} else {
this._stopListening();
}
}
private _stopListening() {
if (this.recognition) {
this.recognition.stop();
private async _startListening() {
this._audio?.pause();
if (!this._audioRecorder) {
this._audioRecorder = new AudioRecorder((audio) => {
if (this._audioBuffer) {
this._audioBuffer.push(audio);
} else {
this._sendAudioChunk(audio);
}
});
}
this._stt_binary_handler_id = undefined;
this._audioBuffer = [];
const userMessage: Message = {
who: "user",
text: "…",
};
this._audioRecorder.start().then(() => {
this._addMessage(userMessage);
this.requestUpdate("_audioRecorder");
});
const hassMessage: Message = {
who: "hass",
text: "…",
};
// To make sure the answer is placed at the right user text, we add it before we process it
try {
const unsub = await runAssistPipeline(
this.hass,
(event) => {
if (event.type === "run-start") {
this._stt_binary_handler_id =
event.data.runner_data.stt_binary_handler_id;
}
// When we start STT stage, the WS has a binary handler
if (event.type === "stt-start" && this._audioBuffer) {
// Send the buffer over the WS to the STT engine.
for (const buffer of this._audioBuffer) {
this._sendAudioChunk(buffer);
}
this._audioBuffer = undefined;
}
// Stop recording if the server is done with STT stage
if (event.type === "stt-end") {
this._stt_binary_handler_id = undefined;
this._stopListening();
userMessage.text = event.data.stt_output.text;
this.requestUpdate("_conversation");
// To make sure the answer is placed at the right user text, we add it before we process it
this._addMessage(hassMessage);
}
if (event.type === "intent-end") {
this._conversationId = event.data.intent_output.conversation_id;
const plain = event.data.intent_output.response.speech?.plain;
if (plain) {
hassMessage.text = plain.speech;
}
this.requestUpdate("_conversation");
}
if (event.type === "tts-end") {
const url = event.data.tts_output.url;
this._audio = new Audio(url);
this._audio.play();
this._audio.addEventListener("ended", this._unloadAudio);
this._audio.addEventListener("pause", this._unloadAudio);
this._audio.addEventListener("canplaythrough", this._playAudio);
this._audio.addEventListener("error", this._audioError);
}
if (event.type === "run-end") {
this._stt_binary_handler_id = undefined;
unsub();
}
if (event.type === "error") {
this._stt_binary_handler_id = undefined;
if (userMessage.text === "…") {
userMessage.text = event.data.message;
userMessage.error = true;
} else {
hassMessage.text = event.data.message;
hassMessage.error = true;
}
this._stopListening();
this.requestUpdate("_conversation");
unsub();
}
},
{
start_stage: "stt",
end_stage: this._pipeline?.tts_engine ? "tts" : "intent",
input: { sample_rate: this._audioRecorder.sampleRate! },
pipeline: this._pipelineId,
conversation_id: this._conversationId,
}
);
} catch (err: any) {
await showAlertDialog(this, {
title: "Error starting pipeline",
text: err.message || err,
});
this._stopListening();
}
}
private _startListening() {
if (!this.recognition) {
this._initRecognition();
private _stopListening() {
this._audioRecorder?.stop();
this.requestUpdate("_audioRecorder");
// We're currently STTing, so finish audio
if (this._stt_binary_handler_id) {
if (this._audioBuffer) {
for (const chunk of this._audioBuffer) {
this._sendAudioChunk(chunk);
}
}
// Send empty message to indicate we're done streaming.
this._sendAudioChunk(new Int16Array());
this._stt_binary_handler_id = undefined;
}
this._audioBuffer = undefined;
}
if (this.results) {
private _sendAudioChunk(chunk: Int16Array) {
this.hass.connection.socket!.binaryType = "arraybuffer";
// eslint-disable-next-line eqeqeq
if (this._stt_binary_handler_id == undefined) {
return;
}
// Turn into 8 bit so we can prefix our handler ID.
const data = new Uint8Array(1 + chunk.length * 2);
data[0] = this._stt_binary_handler_id;
data.set(new Uint8Array(chunk.buffer), 1);
this.results = {
transcript: "",
final: false,
};
this.recognition!.start();
this.hass.connection.socket!.send(data);
}
private _playAudio = () => {
this._audio?.play();
};
private _audioError = () => {
showAlertDialog(this, { title: "Error playing audio." });
this._audio?.removeAttribute("src");
};
private _unloadAudio = () => {
this._audio?.removeAttribute("src");
this._audio = undefined;
};
private _scrollMessagesBottom() {
this._scrollContainer.scrollTo(0, 99999);
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
scrollContainer.scrollTo(0, 99999);
}
private _computeMessageClasses(message: Message) {
@@ -409,18 +576,50 @@ export class HaVoiceCommandDialog extends LitElement {
ha-dialog {
--primary-action-button-flex: 1;
--secondary-action-button-flex: 0;
--mdc-dialog-max-width: 450px;
--mdc-dialog-max-width: 500px;
--mdc-dialog-max-height: 500px;
--dialog-content-padding: 0;
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
--header-height: 64px;
}
ha-header-bar a {
color: var(--primary-text-color);
}
div[slot="title"] {
display: flex;
flex-direction: column;
margin-top: 8px;
}
ha-button-menu {
--mdc-theme-on-primary: var(--text-primary-color);
--mdc-theme-primary: var(--primary-color);
margin: -8px 0 0 -8px;
}
ha-button-menu ha-button {
--mdc-theme-primary: var(--secondary-text-color);
--mdc-typography-button-text-transform: none;
--mdc-typography-button-font-size: unset;
--mdc-typography-button-font-weight: 400;
--mdc-typography-button-letter-spacing: var(
--mdc-typography-headline6-letter-spacing,
0.0125em
);
--mdc-typography-button-line-height: var(
--mdc-typography-headline6-line-height,
2rem
);
--button-height: auto;
}
ha-button-menu ha-button ha-svg-icon {
height: 28px;
margin-left: 4px;
}
ha-button-menu a {
text-decoration: none;
}
ha-textfield {
display: block;
overflow: hidden;
@@ -440,14 +639,20 @@ export class HaVoiceCommandDialog extends LitElement {
padding: 4px;
}
.attribution {
display: block;
color: var(--secondary-text-color);
padding-top: 4px;
margin-bottom: -8px;
}
.messages {
display: block;
height: 300px;
height: 400px;
box-sizing: border-box;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--mdc-dialog-max-width: 100%;
}
.messages {
height: 100%;
}
@@ -512,10 +717,6 @@ export class HaVoiceCommandDialog extends LitElement {
margin-right: 0;
}
.interimTranscript {
color: var(--secondary-text-color);
}
.bouncer {
width: 48px;
height: 48px;

View File

@@ -1,4 +1,8 @@
<meta charset="utf-8">
<link rel='manifest' href='/manifest.json' crossorigin="use-credentials">
<link rel='icon' href='/static/icons/favicon.ico'>
<%= renderTemplate('_style_base') %>
<meta charset="utf-8" />
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="icon" href="/static/icons/favicon.ico" />
<% if (!useWDS) { %>
<% for (const entry of latestEntryJS) { %>
<link rel="modulepreload" href="<%= entry %>" crossorigin="use-credentials" />
<% } %>
<% } %>

View File

@@ -9,13 +9,6 @@
script.src = src;
return script;
}
window.Polymer = {
lazyRegister: true,
useNativeCSSProperties: true,
dom: "shadow",
suppressTemplateNotifications: true,
suppressBindingNotifications: true,
};
window.polymerSkipLoadingFontRoboto = true;
if (!("customElements" in window &&
"content" in document.createElement("template"))) {

View File

@@ -0,0 +1,17 @@
<script>
(function() {
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
<% for (const entry of es5EntryJS) { %>
System.import("<%= entry %>");
<% } %>
}
<% } else { %>
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>");
<% } %>
<% } %>
}
})();
</script>

View File

@@ -2,8 +2,8 @@
<html>
<head>
<title>Home Assistant</title>
<link rel="modulepreload" href="<%= latestPageJS %>" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<%= renderTemplate("_header.html.template") %>
<%= renderTemplate("_style_base.html.template") %>
<style>
.content {
padding: 20px 16px;
@@ -38,36 +38,23 @@
</div>
<ha-authorize><p>Initializing</p></ha-authorize>
</div>
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<%= renderTemplate("_js_base.html.template") %>
<%= renderTemplate("_preload_roboto.html.template") %>
<script crossorigin="use-credentials">
if (!window.globalThis) {
window.globalThis = window;
}
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
import("<%= latestPageJS %>");
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin",
});
}
</script>
<script>
(function() {
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>");
}
<% } else { %>
_ls("<%= es5PageJS %>");
<% } %>
}
})();
</script>
<%= renderTemplate("_script_load_es5.html.template") %>
</body>
</html>

View File

@@ -1,12 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<% if (!useWDS) { %>
<link rel="modulepreload" href="<%= latestCoreJS %>" crossorigin="use-credentials" />
<link rel="modulepreload" href="<%= latestAppJS %>" crossorigin="use-credentials" />
<% } %>
<%= renderTemplate('_header') %>
<title>Home Assistant</title>
<%= renderTemplate("_header.html.template") %>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link
rel="apple-touch-icon"
@@ -36,8 +32,9 @@
<meta name="msapplication-TileColor" content="#03a9f4ff" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" />
<meta name="theme-color" content="{{ theme_color }}" />
<meta name="color-scheme" content="dark light" />
<%= renderTemplate("_style_base.html.template") %>
<style>
html {
background-color: var(--primary-background-color, #fafafa);
@@ -83,20 +80,18 @@
</svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
</div>
<home-assistant></home-assistant>
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<%= renderTemplate("_js_base.html.template") %>
<%= renderTemplate("_preload_roboto.html.template") %>
<script <% if (!useWDS) { %>crossorigin="use-credentials"<% } %>>
if (!window.globalThis) {
window.globalThis = window;
}
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
import("<%= latestCoreJS %>");
import("<%= latestAppJS %>");
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.customPanelJS = "<%= latestCustomPanelJS %>";
window.latestJS = true;
}
@@ -106,7 +101,6 @@
import("{{ extra_module }}");
{%- endfor -%}
</script>
<script>
if (!window.latestJS) {
window.customPanelJS = "<%= es5CustomPanelJS %>";
@@ -115,13 +109,14 @@
_ls("/static/js/s.min.js").onload = function() {
// Although core and app can load in any order, we need to
// force loading core first because it contains polyfills
return System.import("<%= es5CoreJS %>").then(function() {
System.import("<%= es5AppJS %>");
return System.import("<%= es5EntryJS[0] %>").then(function() {
System.import("<%= es5EntryJS[1] %>");
});
}
<% } else { %>
_ls("<%= es5CoreJS %>");
_ls("<%= es5AppJS %>");
<% for (const entry of es5EntryJS) { %>
_ls("<%= entry %>");
<% } %>
<% } %>
}
</script>

View File

@@ -2,12 +2,8 @@
<html>
<head>
<title>Home Assistant</title>
<link
rel="modulepreload"
href="<%= latestPageJS %>"
crossorigin="use-credentials"
/>
<%= renderTemplate('_header') %>
<%= renderTemplate("_header.html.template") %>
<%= renderTemplate("_style_base.html.template") %>
<style>
html {
color: var(--primary-text-color, #212121);
@@ -70,39 +66,25 @@
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
Home Assistant
</div>
<ha-onboarding></ha-onboarding>
</div>
<%= renderTemplate('_js_base') %>
<%= renderTemplate('_preload_roboto') %>
<%= renderTemplate("_js_base.html.template") %>
<%= renderTemplate("_preload_roboto.html.template") %>
<script crossorigin="use-credentials">
if (!window.globalThis) {
window.globalThis = window;
}
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
import("<%= latestPageJS %>");
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin",
});
}
</script>
<script>
(function() {
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5PageJS %>");
}
<% } else { %>
_ls("<%= es5PageJS %>");
<% } %>
}
})();
</script>
<%= renderTemplate("_script_load_es5.html.template") %>
</body>
</html>

View File

@@ -1,10 +1,25 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { createCountryListEl } from "../components/country-datalist";
import { createCurrencyListEl } from "../components/currency-datalist";
import "../components/ha-alert";
import "../components/ha-formfield";
import "../components/ha-radio";
import type { HaRadio } from "../components/ha-radio";
import "../components/ha-textfield";
import type { HaTextField } from "../components/ha-textfield";
import { createLanguageListEl } from "../components/language-datalist";
import "../components/map/ha-locations-editor";
import type {
HaLocationsEditor,
@@ -20,14 +35,7 @@ import { SYMBOL_TO_ISO } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding";
import type { PolymerChangedEvent } from "../polymer-types";
import type { HomeAssistant } from "../types";
import "../components/ha-radio";
import "../components/ha-formfield";
import type { HaRadio } from "../components/ha-radio";
import type { HaTextField } from "../components/ha-textfield";
import "../components/ha-textfield";
import { getLocalLanguage } from "../util/common-translation";
import { createCountryListEl } from "../components/country-datalist";
import { createLanguageListEl } from "../components/language-datalist";
const amsterdam: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
@@ -57,10 +65,18 @@ class OnboardingCoreConfig extends LitElement {
@state() private _country?: ConfigUpdateValues["country"];
@state() private _error?: string;
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
protected render(): TemplateResult {
return html`
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing
}
<p>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.intro",
@@ -114,21 +130,26 @@ class OnboardingCoreConfig extends LitElement {
<div class="row">
<ha-textfield
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
)}
.label=${
this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
) || "Country"
}
name="country"
required
.disabled=${this._working}
.value=${this._countryValue}
@change=${this._handleChange}
></ha-textfield>
<ha-textfield
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
.label=${
this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
) || "Language"
}
name="language"
required
.disabled=${this._working}
.value=${this._languageValue}
@change=${this._handleChange}
@@ -251,7 +272,7 @@ class OnboardingCoreConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
setTimeout(
() => this.shadowRoot!.querySelector("ha-textfield")!.focus(),
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
100
);
this.addEventListener("keypress", (ev) => {
@@ -259,39 +280,43 @@ class OnboardingCoreConfig extends LitElement {
this._save(ev);
}
});
const tzInput = this.shadowRoot!.querySelector(
const tzInput = this.renderRoot.querySelector(
"[name=timeZone]"
) as HaTextField;
tzInput.updateComplete.then(() => {
tzInput.shadowRoot!.appendChild(createTimezoneListEl());
tzInput.renderRoot.appendChild(createTimezoneListEl());
tzInput.formElement.setAttribute("list", "timezones");
});
const curInput = this.shadowRoot!.querySelector(
const curInput = this.renderRoot.querySelector(
"[name=currency]"
) as HaTextField;
curInput.updateComplete.then(() => {
curInput.shadowRoot!.appendChild(
curInput.renderRoot.appendChild(
createCurrencyListEl(this.hass.locale.language)
);
curInput.formElement.setAttribute("list", "currencies");
});
const countryInput = this.shadowRoot!.querySelector(
const countryInput = this.renderRoot.querySelector(
"[name=country]"
) as HaTextField;
countryInput.updateComplete.then(() => {
countryInput.shadowRoot!.appendChild(
countryInput.renderRoot.appendChild(
createCountryListEl(this.hass.locale.language)
);
countryInput.formElement.setAttribute("list", "countries");
});
const langInput = this.shadowRoot!.querySelector(
const langInput = this.renderRoot.querySelector(
"[name=language]"
) as HaTextField;
langInput.updateComplete.then(() => {
langInput.shadowRoot!.appendChild(createLanguageListEl(this.hass));
langInput.renderRoot.appendChild(createLanguageListEl(this.hass));
langInput.renderRoot
.querySelector("#label")
?.classList.add("mdc-floating-label--required");
langInput.formElement.setAttribute("list", "languages");
});
}
@@ -401,7 +426,7 @@ class OnboardingCoreConfig extends LitElement {
}
this._language = getLocalLanguage();
} catch (err: any) {
alert(`Failed to detect location information: ${err.message}`);
this._error = `Failed to detect location information: ${err.message}`;
} finally {
this._working = false;
}
@@ -430,7 +455,7 @@ class OnboardingCoreConfig extends LitElement {
});
} catch (err: any) {
this._working = false;
alert(`Failed to save: ${err.message}`);
this._error = err.message;
}
}

View File

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

View File

@@ -498,12 +498,22 @@ class DialogCalendarEventEditor extends LitElement {
this._submitting = false;
return;
}
const eventData = this._calculateData();
if (eventData.rrule && range === RecurrenceRange.THISEVENT) {
// Updates to a single instance of a recurring event by definition
// cannot change the recurrence rule and doing so would be invalid.
// It is difficult to detect if the user changed the recurrence rule
// since updating the date may change it implicitly (e.g. day of week
// of the event changes) so we just assume the users intent based on
// recurrence range and drop any other rrule changes.
eventData.rrule = undefined;
}
try {
await updateCalendarEvent(
this.hass!,
this._calendarId!,
entry.uid!,
this._calculateData(),
eventData,
entry.recurrence_id || "",
range!
);

View File

@@ -520,7 +520,7 @@ export class HaAutomationTrace extends LitElement {
}
.main {
height: calc(100% - var(--header-height));
min-height: calc(100% - var(--header-height));
display: flex;
background-color: var(--card-background-color);
direction: ltr;

View File

@@ -12,12 +12,16 @@ import "../../../../components/ha-icon-next";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-textfield";
import { cloudLogin } from "../../../../data/cloud";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import "../../../../styles/polymer-ha-style";
import { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -210,10 +214,24 @@ export class CloudLogin extends LitElement {
this._requestInProgress = true;
try {
await cloudLogin(this.hass, email, password);
const result = await cloudLogin(this.hass, email, password);
fireEvent(this, "ha-refresh-cloud-status");
this.email = "";
this._password = "";
if (result.cloud_pipeline) {
if (
await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.login.cloud_pipeline_text"
),
})
) {
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
}
}
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "PasswordChangeRequired") {

View File

@@ -6,13 +6,16 @@ import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import { getCountryOptions } from "../../../components/country-datalist";
import { getCurrencyOptions } from "../../../components/currency-datalist";
import "../../../components/ha-alert";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../components/ha-checkbox";
import "../../../components/ha-formfield";
import "../../../components/ha-language-picker";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
@@ -22,13 +25,10 @@ import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { SYMBOL_TO_ISO } from "../../../data/currency";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-alert";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HaCheckbox } from "../../../components/ha-checkbox";
import "../../../components/ha-checkbox";
@customElement("ha-config-section-general")
class HaConfigSectionGeneral extends LitElement {
@@ -54,8 +54,6 @@ class HaConfigSectionGeneral extends LitElement {
@state() private _location?: [number, number];
@state() private _languages?: { value: string; label: string }[];
@state() private _error?: string;
@state() private _updateUnits?: boolean;
@@ -255,25 +253,19 @@ class HaConfigSectionGeneral extends LitElement {
</mwc-list-item>`
)}</ha-select
>
<ha-select
<ha-language-picker
.hass=${this.hass}
nativeName
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
fixedMenuPosition
naturalMenuWidth
.disabled=${disabled}
.value=${this._language}
.disabled=${disabled}
@closed=${stopPropagation}
@change=${this._handleChange}
>
${this._languages?.map(
({ value, label }) =>
html`<mwc-list-item .value=${value}
>${label}</mwc-list-item
>`
)}</ha-select
@value-changed=${this._handleLanguageChange}
>
</ha-language-picker>
</div>
${this.narrow
? html`
@@ -330,25 +322,10 @@ class HaConfigSectionGeneral extends LitElement {
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
this._name = this.hass.config.location_name;
this._updateUnits = true;
this._computeLanguages();
}
private _computeLanguages() {
if (!this.hass.translationMetadata?.translations) {
return;
}
this._languages = Object.entries(this.hass.translationMetadata.translations)
.sort((a, b) =>
caseInsensitiveStringCompare(
a[1].nativeName,
b[1].nativeName,
this.hass.locale.language
)
)
.map(([value, metaData]) => ({
value,
label: metaData.nativeName,
}));
private _handleLanguageChange(ev) {
this._language = ev.detail.value;
}
private _handleChange(ev) {

View File

@@ -3,8 +3,8 @@ import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
@@ -28,8 +28,9 @@ import "../../../helpers/forms/ha-input_select-form";
import "../../../helpers/forms/ha-input_text-form";
import "../../../helpers/forms/ha-schedule-form";
import "../../../helpers/forms/ha-timer-form";
import "../../entity-registry-basic-editor";
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
import "../../../voice-assistants/entity-voice-settings";
import "../../entity-registry-settings-editor";
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
@customElement("entity-settings-helper-tab")
export class EntityRegistrySettingsHelper extends LitElement {
@@ -45,8 +46,8 @@ export class EntityRegistrySettingsHelper extends LitElement {
@state() private _componentLoaded?: boolean;
@query("ha-registry-basic-editor")
private _registryEditor?: HaEntityRegistryBasicEditor;
@query("entity-registry-settings-editor")
private _registryEditor?: EntityRegistrySettingsEditor;
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
@@ -75,7 +76,9 @@ export class EntityRegistrySettingsHelper extends LitElement {
const stateObj = this.hass.states[this.entry.entity_id];
return html`
<div class="form">
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${!this._componentLoaded
? this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
@@ -93,10 +96,14 @@ export class EntityRegistrySettingsHelper extends LitElement {
})}
</span>
`}
<ha-registry-basic-editor
<entity-registry-settings-editor
.hass=${this.hass}
.entry=${this.entry}
></ha-registry-basic-editor>
.disabled=${this._submitting}
@change=${this._entityRegistryChanged}
hideName
hideIcon
></entity-registry-settings-editor>
</div>
<div class="buttons">
<mwc-button
@@ -117,6 +124,10 @@ export class EntityRegistrySettingsHelper extends LitElement {
`;
}
private _entityRegistryChanged() {
this._error = undefined;
}
private _valueChanged(ev: CustomEvent): void {
this._error = undefined;
this._item = ev.detail.value;
@@ -137,7 +148,7 @@ export class EntityRegistrySettingsHelper extends LitElement {
this._item
);
}
await this._registryEditor?.updateEntry();
await this._registryEditor!.updateEntry();
fireEvent(this, "close-dialog");
} catch (err: any) {
this._error = err.message || "Unknown error";

View File

@@ -1,376 +0,0 @@
import "@material/mwc-formfield/mwc-formfield";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiPencil } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-radio";
import "../../../components/ha-switch";
import "../../../components/ha-textfield";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import {
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
@customElement("ha-registry-basic-editor")
export class HaEntityRegistryBasicEditor extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@state() private _origEntityId!: string;
@state() private _entityId!: string;
@state() private _areaId?: string | null;
@state() private _disabledBy!: EntityRegistryEntry["disabled_by"];
@state() private _hiddenBy!: string | null;
private _deviceLookup?: Record<string, DeviceRegistryEntry>;
@state() private _device?: DeviceRegistryEntry;
@state() private _submitting = false;
private _handleAliasesClicked(ev: CustomEvent) {
if (ev.detail.index !== 0) return;
const stateObj = this.hass.states[this.entry.entity_id];
const name =
(stateObj && computeStateName(stateObj)) || this.entry.entity_id;
showAliasesDialog(this, {
name,
aliases: this.entry!.aliases,
updateAliases: async (aliases: string[]) => {
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
{ aliases }
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
},
});
}
public async updateEntry(): Promise<void> {
this._submitting = true;
const params: Partial<EntityRegistryEntryUpdateParams> = {
new_entity_id: this._entityId.trim(),
area_id: this._areaId || null,
};
if (
this.entry.disabled_by !== this._disabledBy &&
(this._disabledBy === null || this._disabledBy === "user")
) {
params.disabled_by = this._disabledBy;
}
if (
this.entry.hidden_by !== this._hiddenBy &&
(this._hiddenBy === null || this._hiddenBy === "user")
) {
params.hidden_by = this._hiddenBy;
}
try {
const result = await updateEntityRegistryEntry(
this.hass!,
this._origEntityId,
params
);
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_restart_confirm"
),
});
}
if (result.reload_delay) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_delay_confirm",
"delay",
result.reload_delay
),
});
}
} finally {
this._submitting = false;
}
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._deviceLookup = {};
for (const device of devices) {
this._deviceLookup[device.id] = device;
}
if (!this._device && this.entry.device_id) {
this._device = this._deviceLookup[this.entry.device_id];
}
}),
];
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (!changedProperties.has("entry")) {
return;
}
if (this.entry) {
this._origEntityId = this.entry.entity_id;
this._entityId = this.entry.entity_id;
this._disabledBy = this.entry.disabled_by;
this._hiddenBy = this.entry.hidden_by;
this._areaId = this.entry.area_id;
this._device =
this.entry.device_id && this._deviceLookup
? this._deviceLookup[this.entry.device_id]
: undefined;
}
}
protected render() {
if (
!this.hass ||
!this.entry ||
this.entry.entity_id !== this._origEntityId
) {
return nothing;
}
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
return html`
<ha-textfield
error-message="Domain needs to stay the same"
.value=${this._entityId}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_id"
)}
.invalid=${invalidDomainUpdate}
.disabled=${this._submitting}
@input=${this._entityIdChanged}
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId || undefined}
.placeholder=${this._device?.area_id || undefined}
@value-changed=${this._areaPicked}
></ha-area-picker>
<ha-expansion-panel
.header=${this.hass.localize(
"ui.dialogs.entity_registry.editor.advanced"
)}
outlined
>
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.entity_status"
)}:
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_cause",
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`
)
)
: ""}
</div>
<div class="row">
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_label"
)}
>
<ha-radio
name="hiddendisabled"
value="enabled"
.checked=${!this._hiddenBy && !this._disabledBy}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_label"
)}
>
<ha-radio
name="hiddendisabled"
value="hidden"
.checked=${this._hiddenBy !== null}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
<mwc-formfield
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.disabled_label"
)}
>
<ha-radio
name="hiddendisabled"
value="disabled"
.checked=${this._disabledBy !== null}
.disabled=${!!this._device?.disabled_by ||
(this._disabledBy !== null &&
!(
this._disabledBy === "user" ||
this._disabledBy === "integration"
))}
@change=${this._viewStatusChanged}
></ha-radio>
</mwc-formfield>
</div>
${this._disabledBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.enabled_description"
)}
</div>
`
: this._hiddenBy !== null
? html`
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.hidden_description"
)}
</div>
`
: ""}
<div class="label">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_section"
)}
</div>
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
<span>
${this.entry.aliases.length > 0
? this.hass.localize(
"ui.dialogs.entity_registry.editor.configured_aliases",
{ count: this.entry.aliases.length }
)
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<span slot="secondary">
${[...this.entry.aliases]
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
.join(", ")}
</span>
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
</mwc-list-item>
</mwc-list>
<div class="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.aliases_description"
)}
</div>
</ha-expansion-panel>
`;
}
private _areaPicked(ev: CustomEvent) {
this._areaId = ev.detail.value;
}
private _entityIdChanged(ev): void {
this._entityId = ev.target.value;
}
private _viewStatusChanged(ev: CustomEvent): void {
switch ((ev.target as any).value) {
case "enabled":
this._disabledBy = null;
this._hiddenBy = null;
break;
case "disabled":
this._disabledBy = "user";
this._hiddenBy = null;
break;
case "hidden":
this._hiddenBy = "user";
this._disabledBy = null;
break;
}
}
static get styles() {
return css`
ha-switch {
margin-right: 16px;
}
.row {
margin-top: 8px;
color: var(--primary-text-color);
display: flex;
align-items: center;
}
.secondary {
color: var(--secondary-text-color);
}
ha-textfield {
display: block;
margin-bottom: 8px;
}
ha-expansion-panel {
margin-top: 8px;
}
.label {
margin-top: 16px;
}
.aliases {
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
--mdc-icon-button-size: 24px;
overflow: hidden;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-registry-basic-editor": HaEntityRegistryBasicEditor;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -92,14 +92,24 @@ class AddIntegrationDialog extends LitElement {
private _height?: number;
public showDialog(params?: AddIntegrationDialogParams): void {
this._load();
public async showDialog(params?: AddIntegrationDialogParams): Promise<void> {
const loadPromise = this._load();
this._open = true;
this._pickedBrand = params?.brand;
this._initialFilter = params?.initialFilter;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
if (params?.domain) {
this._createFlow(params.domain);
}
if (params?.brand) {
await loadPromise;
const brand = this._integrations?.[params.brand];
if (brand && "integrations" in brand && brand.integrations) {
this._fetchFlowsInProgress(Object.keys(brand.integrations));
}
}
}
public closeDialog() {
@@ -543,7 +553,7 @@ class AddIntegrationDialog extends LitElement {
}
if (integration.config_flow) {
this._createFlow(integration);
this._createFlow(integration.domain);
return;
}
@@ -563,25 +573,20 @@ class AddIntegrationDialog extends LitElement {
showYamlIntegrationDialog(this, { manifest });
}
private async _createFlow(integration: IntegrationListItem) {
const flowsInProgress = await this._fetchFlowsInProgress([
integration.domain,
]);
private async _createFlow(domain: string) {
const flowsInProgress = await this._fetchFlowsInProgress([domain]);
if (flowsInProgress?.length) {
this._pickedBrand = integration.domain;
this._pickedBrand = domain;
return;
}
const manifest = await fetchIntegrationManifest(
this.hass,
integration.domain
);
const manifest = await fetchIntegrationManifest(this.hass, domain);
this.closeDialog();
showConfigFlowDialog(this, {
startFlowHandler: integration.domain,
startFlowHandler: domain,
showAdvanced: this.hass.userData?.showAdvanced,
manifest,
});

View File

@@ -75,8 +75,6 @@ export class DialogYamlIntegration extends LitElement {
text-decoration: none;
}
ha-dialog {
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
/* Place above other dialogs */
--dialog-z-index: 104;
}

View File

@@ -40,10 +40,6 @@ import {
subscribeConfigFlowInProgress,
} from "../../../data/config_flow";
import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
import {
EntityRegistryEntry,
@@ -53,13 +49,13 @@ import {
domainToName,
fetchIntegrationManifest,
fetchIntegrationManifests,
IntegrationManifest,
IntegrationLogInfo,
IntegrationManifest,
subscribeLogInfo,
} from "../../../data/integration";
import {
getIntegrationDescriptions,
findIntegration,
getIntegrationDescriptions,
} from "../../../data/integrations";
import { scanUSBDevices } from "../../../data/usb";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
@@ -139,9 +135,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@state()
private _entityRegistryEntries: EntityRegistryEntry[] = [];
@state()
private _deviceRegistryEntries: DeviceRegistryEntry[] = [];
@state()
private _manifests: Record<string, IntegrationManifest> = {};
@@ -168,9 +161,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
subscribeEntityRegistry(this.hass.connection, (entries) => {
this._entityRegistryEntries = entries;
}),
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._deviceRegistryEntries = entries;
}),
subscribeConfigFlowInProgress(this.hass, async (flowsInProgress) => {
const integrations: Set<string> = new Set();
const manifests: Set<string> = new Set();
@@ -513,7 +503,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.items=${items}
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
></ha-integration-card> `
)
: ""}
@@ -527,7 +516,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.items=${items}
.manifest=${this._manifests[domain]}
.entityRegistryEntries=${this._entityRegistryEntries}
.deviceRegistryEntries=${this._deviceRegistryEntries}
.supportsDiagnostics=${this._diagnosticHandlers
? this._diagnosticHandlers[domain]
: false}
@@ -731,13 +719,8 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}),
})
) {
showConfigFlowDialog(this, {
dialogClosedCallback: () => {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this.hass, domain),
showAdvanced: this.hass.userData?.showAdvanced,
showAddIntegrationDialog(this, {
domain,
});
}
return;

View File

@@ -1,5 +1,5 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-list";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import {
mdiAlertCircle,
@@ -20,8 +20,6 @@ import {
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
@@ -30,6 +28,7 @@ import memoizeOne from "memoize-one";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-list-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
@@ -96,9 +95,6 @@ export class HaIntegrationCard extends LitElement {
@property({ attribute: false })
public entityRegistryEntries!: EntityRegistryEntry[];
@property({ attribute: false })
public deviceRegistryEntries!: DeviceRegistryEntry[];
@property() public selectedConfigEntryId?: string;
@property({ type: Boolean }) public entryDisabled = false;
@@ -174,20 +170,19 @@ export class HaIntegrationCard extends LitElement {
private _renderGroupedIntegration(): TemplateResult {
return html`
<paper-listbox class="ha-scrollbar">
<mwc-list class="ha-scrollbar">
${this.items.map(
(item) =>
html`<paper-item
html`<ha-list-item
hasMeta
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</paper-item-body
>
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}
${item.state === "setup_in_progress"
? html`<span>
? html`<span slot="meta">
<ha-svg-icon
class="info"
.path=${mdiProgressHelper}
@@ -198,9 +193,8 @@ export class HaIntegrationCard extends LitElement {
)}
</simple-tooltip>
</span>`
: ""}
${ERROR_STATES.includes(item.state)
? html`<span>
: ERROR_STATES.includes(item.state)
? html`<span slot="meta">
<ha-svg-icon
class="error"
.path=${item.state === "setup_retry"
@@ -213,17 +207,16 @@ export class HaIntegrationCard extends LitElement {
)}
</simple-tooltip>
</span>`
: ""}
<ha-icon-next></ha-icon-next>
</paper-item>`
: html`<ha-icon-next slot="meta"></ha-icon-next>`}
</ha-list-item>`
)}
</paper-listbox>
</mwc-list>
`;
}
private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult {
const devices = this._getDevices(item, this.deviceRegistryEntries);
const services = this._getServices(item, this.deviceRegistryEntries);
const devices = this._getDevices(item, this.hass.devices);
const services = this._getServices(item, this.hass.devices);
const entities = this._getEntities(item, this.entityRegistryEntries);
let stateText: Parameters<typeof this.hass.localize> | undefined;
@@ -380,7 +373,7 @@ export class HaIntegrationCard extends LitElement {
RECOVERABLE_STATES.includes(item.state) &&
item.supports_unload &&
item.source !== "system"
? html`<mwc-list-item
? html`<ha-list-item
@request-selected=${this._handleReload}
graphic="icon"
>
@@ -388,22 +381,22 @@ export class HaIntegrationCard extends LitElement {
"ui.panel.config.integrations.config_entry.reload"
)}
<ha-svg-icon slot="graphic" .path=${mdiReload}></ha-svg-icon>
</mwc-list-item>`
</ha-list-item>`
: ""}
<mwc-list-item @request-selected=${this._handleRename} graphic="icon">
<ha-list-item @request-selected=${this._handleRename} graphic="icon">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</mwc-list-item>
</ha-list-item>
${this.supportsDiagnostics && item.state === "loaded"
? html`<a
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
target="_blank"
@click=${this._signUrl}
>
<mwc-list-item graphic="icon">
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.download_diagnostics"
)}
@@ -411,11 +404,11 @@ export class HaIntegrationCard extends LitElement {
slot="graphic"
.path=${mdiDownload}
></ha-svg-icon>
</mwc-list-item>
</ha-list-item>
</a>`
: ""}
${this.logInfo
? html`<mwc-list-item
? html`<ha-list-item
@request-selected=${this.logInfo.level === LogSeverity.DEBUG
? this._handleDisableDebugLogging
: this._handleEnableDebugLogging}
@@ -434,7 +427,7 @@ export class HaIntegrationCard extends LitElement {
? mdiBugStop
: mdiBugPlay}
></ha-svg-icon>
</mwc-list-item>`
</ha-list-item>`
: ""}
${this.manifest &&
(this.manifest.is_built_in ||
@@ -453,7 +446,7 @@ export class HaIntegrationCard extends LitElement {
rel="noreferrer"
target="_blank"
>
<mwc-list-item graphic="icon" hasMeta>
<ha-list-item graphic="icon" hasMeta>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.documentation"
)}
@@ -462,7 +455,7 @@ export class HaIntegrationCard extends LitElement {
.path=${mdiBookshelf}
></ha-svg-icon>
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</mwc-list-item>
</ha-list-item>
</a>`
: ""}
${this.manifest &&
@@ -472,19 +465,19 @@ export class HaIntegrationCard extends LitElement {
rel="noreferrer"
target="_blank"
>
<mwc-list-item graphic="icon" hasMeta>
<ha-list-item graphic="icon" hasMeta>
${this.hass.localize(
"ui.panel.config.integrations.config_entry.known_issues"
)}
<ha-svg-icon slot="graphic" .path=${mdiBug}></ha-svg-icon>
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</mwc-list-item>
</ha-list-item>
</a>`
: ""}
<li divider role="separator"></li>
<mwc-list-item
<ha-list-item
@request-selected=${this._handleSystemOptions}
graphic="icon"
>
@@ -492,9 +485,9 @@ export class HaIntegrationCard extends LitElement {
"ui.panel.config.integrations.config_entry.system_options"
)}
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
</mwc-list-item>
</ha-list-item>
${item.disabled_by === "user"
? html`<mwc-list-item
? html`<ha-list-item
@request-selected=${this._handleEnable}
graphic="icon"
>
@@ -503,9 +496,9 @@ export class HaIntegrationCard extends LitElement {
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
</mwc-list-item>`
</ha-list-item>`
: item.source !== "system"
? html`<mwc-list-item
? html`<ha-list-item
class="warning"
@request-selected=${this._handleDisable}
graphic="icon"
@@ -516,10 +509,10 @@ export class HaIntegrationCard extends LitElement {
class="warning"
.path=${mdiStopCircleOutline}
></ha-svg-icon>
</mwc-list-item>`
</ha-list-item>`
: ""}
${item.source !== "system"
? html`<mwc-list-item
? html`<ha-list-item
class="warning"
@request-selected=${this._handleDelete}
graphic="icon"
@@ -532,7 +525,7 @@ export class HaIntegrationCard extends LitElement {
class="warning"
.path=${mdiDelete}
></ha-svg-icon>
</mwc-list-item>`
</ha-list-item>`
: ""}
</ha-button-menu>
</div>
@@ -606,12 +599,12 @@ export class HaIntegrationCard extends LitElement {
private _getDevices = memoizeOne(
(
configEntry: ConfigEntry,
deviceRegistryEntries: DeviceRegistryEntry[]
deviceRegistryEntries: HomeAssistant["devices"]
): DeviceRegistryEntry[] => {
if (!deviceRegistryEntries) {
return [];
}
return deviceRegistryEntries.filter(
return Object.values(deviceRegistryEntries).filter(
(device) =>
device.config_entries.includes(configEntry.entry_id) &&
device.entry_type !== "service"
@@ -622,12 +615,12 @@ export class HaIntegrationCard extends LitElement {
private _getServices = memoizeOne(
(
configEntry: ConfigEntry,
deviceRegistryEntries: DeviceRegistryEntry[]
deviceRegistryEntries: HomeAssistant["devices"]
): DeviceRegistryEntry[] => {
if (!deviceRegistryEntries) {
return [];
}
return deviceRegistryEntries.filter(
return Object.values(deviceRegistryEntries).filter(
(device) =>
device.config_entries.includes(configEntry.entry_id) &&
device.entry_type === "service"
@@ -1001,16 +994,16 @@ export class HaIntegrationCard extends LitElement {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
}
paper-listbox {
mwc-list {
border-radius: 0 0 var(--ha-card-border-radius, 16px)
var(--ha-card-border-radius, 16px);
}
@media (min-width: 563px) {
ha-card.group {
position: relative;
min-height: 164px;
min-height: 195px;
}
paper-listbox {
mwc-list {
position: absolute;
top: 64px;
left: 0;
@@ -1018,25 +1011,24 @@ export class HaIntegrationCard extends LitElement {
bottom: 0;
overflow: auto;
}
.disabled paper-listbox {
.disabled mwc-list {
top: 88px;
}
}
paper-item {
cursor: pointer;
min-height: 35px;
}
paper-item-body {
ha-list-item {
word-wrap: break-word;
display: -webkit-box;
display: -webkit-flex;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
mwc-list-item ha-svg-icon {
ha-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
ha-icon-next {
width: 24px;
}
ha-svg-icon[slot="meta"] {
width: 18px;
height: 18px;

View File

@@ -3,6 +3,7 @@ import { IntegrationManifest } from "../../../data/integration";
export interface AddIntegrationDialogParams {
brand?: string;
domain?: string;
initialFilter?: string;
}

View File

@@ -102,7 +102,12 @@ export class HaBlueprintScriptEditor extends LitElement {
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<span slot="description">${value?.description}</span>
<ha-markdown
slot="description"
class="card-content"
breaks
.content=${value?.description}
></ha-markdown>
${value?.selector
? html`<ha-selector
.hass=${this.hass}

View File

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

View File

@@ -0,0 +1,114 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { SchemaUnion } from "../../../../components/ha-form/types";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { HomeAssistant } from "../../../../types";
@customElement("assist-pipeline-detail-config")
export class AssistPipelineDetailConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public data?: Partial<AssistPipeline>;
@property() public supportedLanguages?: string[];
public async focus() {
await this.updateComplete;
const input = this.renderRoot?.querySelector("ha-form");
input?.focus();
}
private _schema = memoizeOne(
(supportedLanguages?: string[]) =>
[
{
name: "",
type: "grid",
schema: [
{
name: "name",
required: true,
selector: {
text: {},
},
},
{
name: "language",
required: true,
selector: {
language: {
languages: supportedLanguages ?? [],
},
},
},
] as const,
},
] as const
);
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}`
);
protected render() {
return html`
<div class="section">
<div class="intro">
<h3>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.config.title`
)}
</h3>
<p>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.config.description`
)}
</p>
</div>
<ha-form
.schema=${this._schema(this.supportedLanguages)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
></ha-form>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
.section {
border: 1px solid var(--divider-color);
border-radius: 8px;
box-sizing: border-box;
padding: 16px;
}
.intro {
margin-bottom: 16px;
}
h3 {
font-weight: normal;
font-size: 22px;
line-height: 28px;
margin-top: 0;
margin-bottom: 4px;
}
p {
color: var(--secondary-text-color);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
margin-top: 0;
margin-bottom: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"assist-pipeline-detail-config": AssistPipelineDetailConfig;
}
}

View File

@@ -0,0 +1,125 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalizeKeys } from "../../../../common/translations/localize";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { HomeAssistant } from "../../../../types";
import "../../../../components/ha-form/ha-form";
import { fireEvent } from "../../../../common/dom/fire_event";
@customElement("assist-pipeline-detail-conversation")
export class AssistPipelineDetailConversation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public data?: Partial<AssistPipeline>;
@state() private _supportedLanguages?: "*" | string[];
private _schema = memoizeOne(
(language?: string, supportedLanguages?: "*" | string[]) =>
[
{
name: "",
type: "grid",
schema: [
{
name: "conversation_engine",
required: true,
selector: {
conversation_agent: {
language,
},
},
},
supportedLanguages !== "*" && supportedLanguages?.length
? {
name: "conversation_language",
required: true,
selector: {
language: { languages: supportedLanguages, no_sort: true },
},
}
: { name: "", type: "constant" },
] as const,
},
] as const
);
private _computeLabel = (schema): string =>
schema.name
? this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys
)
: "";
protected render() {
return html`
<div class="section">
<div class="intro">
<h3>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.conversation.title`
)}
</h3>
<p>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.conversation.description`
)}
</p>
</div>
<ha-form
.schema=${this._schema(this.data?.language, this._supportedLanguages)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
@supported-languages-changed=${this._supportedLanguagesChanged}
></ha-form>
</div>
`;
}
private _supportedLanguagesChanged(ev) {
if (ev.detail.value === "*") {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.conversation_language = "*";
fireEvent(this, "value-changed", { value });
}, 0);
}
this._supportedLanguages = ev.detail.value;
}
static get styles(): CSSResultGroup {
return css`
.section {
border: 1px solid var(--divider-color);
border-radius: 8px;
box-sizing: border-box;
padding: 16px;
}
.intro {
margin-bottom: 16px;
}
h3 {
font-weight: normal;
font-size: 22px;
line-height: 28px;
margin-top: 0;
margin-bottom: 4px;
}
p {
color: var(--secondary-text-color);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
margin-top: 0;
margin-bottom: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"assist-pipeline-detail-conversation": AssistPipelineDetailConversation;
}
}

View File

@@ -0,0 +1,115 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalizeKeys } from "../../../../common/translations/localize";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { HomeAssistant } from "../../../../types";
import "../../../../components/ha-form/ha-form";
@customElement("assist-pipeline-detail-stt")
export class AssistPipelineDetailSTT extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public data?: Partial<AssistPipeline>;
@state() private _supportedLanguages?: string[];
private _schema = memoizeOne(
(language?: string, supportedLanguages?: string[]) =>
[
{
name: "",
type: "grid",
schema: [
{
name: "stt_engine",
selector: {
stt: {
language,
},
},
},
supportedLanguages?.length
? {
name: "stt_language",
required: true,
selector: {
language: { languages: supportedLanguages, no_sort: true },
},
}
: { name: "", type: "constant" },
] as const,
},
] as const
);
private _computeLabel = (schema): string =>
schema.name
? this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys
)
: "";
protected render() {
return html`
<div class="section">
<div class="intro">
<h3>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.stt.title`
)}
</h3>
<p>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.stt.description`
)}
</p>
</div>
<ha-form
.schema=${this._schema(this.data?.language, this._supportedLanguages)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
@supported-languages-changed=${this._supportedLanguagesChanged}
></ha-form>
</div>
`;
}
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
}
static get styles(): CSSResultGroup {
return css`
.section {
border: 1px solid var(--divider-color);
border-radius: 8px;
box-sizing: border-box;
padding: 16px;
}
.intro {
margin-bottom: 16px;
}
h3 {
font-weight: normal;
font-size: 22px;
line-height: 28px;
margin-top: 0;
margin-bottom: 4px;
}
p {
color: var(--secondary-text-color);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
margin-top: 0;
margin-bottom: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"assist-pipeline-detail-stt": AssistPipelineDetailSTT;
}
}

View File

@@ -0,0 +1,166 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-form/ha-form";
import { AssistPipeline } from "../../../../data/assist_pipeline";
import { showTTSTryDialog } from "../../../../dialogs/tts-try/show-dialog-tts-try";
import { HomeAssistant } from "../../../../types";
@customElement("assist-pipeline-detail-tts")
export class AssistPipelineDetailTTS extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public data?: Partial<AssistPipeline>;
@state() private _supportedLanguages?: string[];
private _schema = memoizeOne(
(language?: string, supportedLanguages?: string[]) =>
[
{
name: "",
type: "grid",
schema: [
{
name: "tts_engine",
selector: {
tts: {
language,
},
},
},
supportedLanguages?.length
? {
name: "tts_language",
required: true,
selector: {
language: { languages: supportedLanguages, no_sort: true },
},
}
: { name: "", type: "constant" },
{
name: "tts_voice",
selector: {
tts_voice: {},
},
context: { language: "tts_language", engineId: "tts_engine" },
required: true,
},
] as const,
},
] as const
);
private _computeLabel = (schema): string =>
schema.name
? this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys
)
: "";
protected render() {
return html`
<div class="section">
<div class="content">
<div class="intro">
<h3>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.tts.title`
)}
</h3>
<p>
${this.hass.localize(
`ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.tts.description`
)}
</p>
</div>
<ha-form
.schema=${this._schema(
this.data?.language,
this._supportedLanguages
)}
.data=${this.data}
.hass=${this.hass}
.computeLabel=${this._computeLabel}
@supported-languages-changed=${this._supportedLanguagesChanged}
></ha-form>
</div>
${
this.data?.tts_engine
? html`<div class="footer">
<ha-button
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.try_tts"
)}
@click=${this._preview}
>
</ha-button>
</div>`
: nothing
}
</div>
</div>
`;
}
private async _preview() {
if (!this.data) return;
const engine = this.data.tts_engine;
const language = this.data.tts_language || undefined;
const voice = this.data.tts_voice || undefined;
if (!engine) return;
showTTSTryDialog(this, {
engine,
language,
voice,
});
}
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
}
static get styles(): CSSResultGroup {
return css`
.section {
border: 1px solid var(--divider-color);
border-radius: 8px;
}
.content {
padding: 16px;
}
.intro {
margin-bottom: 16px;
}
h3 {
font-weight: normal;
font-size: 22px;
line-height: 28px;
margin-top: 0;
margin-bottom: 4px;
}
p {
color: var(--secondary-text-color);
font-size: var(--mdc-typography-body2-font-size, 0.875rem);
margin-top: 0;
margin-bottom: 0;
}
.footer {
border-top: 1px solid var(--divider-color);
padding: 8px 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"assist-pipeline-detail-tts": AssistPipelineDetailTTS;
}
}

View File

@@ -2,42 +2,57 @@ import "@material/mwc-list/mwc-list";
import { mdiHelpCircle, mdiPlus, mdiStar } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatLanguageCode } from "../../../common/language/format_language";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-svg-icon";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import "../../../components/ha-button";
import {
AssistPipeline,
createAssistPipeline,
deleteAssistPipeline,
fetchAssistPipelines,
updateAssistPipeline,
AssistPipeline,
listAssistPipelines,
setAssistPipelinePreferred,
updateAssistPipeline,
} from "../../../data/assist_pipeline";
import { CloudStatus } from "../../../data/cloud";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
import { brandsUrl } from "../../../util/brands-url";
import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail";
export class AssistPref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>;
@state() private _pipelines: AssistPipeline[] = [];
@state() private _preferred: string | null = null;
@property() public cloudStatus?: CloudStatus;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
fetchAssistPipelines(this.hass).then((pipelines) => {
listAssistPipelines(this.hass).then((pipelines) => {
this._pipelines = pipelines.pipelines;
this._preferred = pipelines.preferred_pipeline;
});
}
private _exposedEntities = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.conversation?.should_expose
).length
);
protected render() {
return html`
<ha-card outlined>
@@ -76,7 +91,9 @@ export class AssistPref extends LitElement {
.id=${pipeline.id}
>
${pipeline.name}
<span slot="secondary">${pipeline.language}</span>
<span slot="secondary">
${formatLanguageCode(pipeline.language, this.hass.locale)}
</span>
${this._preferred === pipeline.id
? html`<ha-svg-icon
slot="meta"
@@ -100,7 +117,12 @@ export class AssistPref extends LitElement {
>
<ha-button>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.manage_entities"
"ui.panel.config.voice_assistants.assistants.pipeline.exposed_entities",
{
number: this.extEntities
? this._exposedEntities(this.extEntities)
: 0,
}
)}
</ha-button>
</a>
@@ -122,6 +144,8 @@ export class AssistPref extends LitElement {
private async _openDialog(pipeline?: AssistPipeline): Promise<void> {
showVoiceAssistantPipelineDetailDialog(this, {
cloudActiveSubscription:
this.cloudStatus?.logged_in && this.cloudStatus.active_subscription,
pipeline,
preferred: pipeline?.id === this._preferred,
createPipeline: async (values) => {

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyFilter } from "../../../common/entity/entity_filter";
import "../../../components/ha-alert";
@@ -10,6 +11,7 @@ import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import {
getExposeNewEntities,
setExposeNewEntities,
@@ -20,10 +22,19 @@ import { brandsUrl } from "../../../util/brands-url";
export class CloudAlexaPref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>;
@property() public cloudStatus?: CloudStatusLoggedIn;
@state() private _exposeNew?: boolean;
private _exposedEntities = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.["cloud.alexa"]?.should_expose
).length
);
protected willUpdate() {
if (!this.hasUpdated) {
getExposeNewEntities(this.hass, "cloud.alexa").then((value) => {
@@ -159,17 +170,28 @@ export class CloudAlexaPref extends LitElement {
`
: ""}`}
</div>
<div class="card-actions">
<a
href="/config/voice-assistants/expose?assistants=cloud.alexa&historyBack"
>
<mwc-button
>${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.manage_entities"
)}</mwc-button
>
</a>
</div>
${alexa_enabled
? html`<div class="card-actions">
<a
href="/config/voice-assistants/expose?assistants=cloud.alexa&historyBack"
>
<mwc-button>
${manualConfig
? this.hass!.localize(
"ui.panel.config.cloud.account.alexa.show_entities"
)
: this.hass.localize(
"ui.panel.config.cloud.account.alexa.exposed_entities",
{
number: this.extEntities
? this._exposedEntities(this.extEntities)
: 0,
}
)}
</mwc-button>
</a>
</div>`
: nothing}
</ha-card>
`;
}

View File

@@ -0,0 +1,206 @@
import { mdiMicrophoneMessage, mdiOpenInNew } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-card";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import "../../../components/ha-svg-icon";
import "../../../components/ha-button";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@customElement("cloud-discover")
export class CloudDiscover extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
protected render() {
return html`
<ha-card outlined>
<div class="card-content">
<h1 class="header">
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.header"
)}
</h1>
<div class="features">
<div class="feature">
<div class="logos">
<img
alt="Google Assistant"
src=${brandsUrl({
domain: "google_assistant",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<img
alt="Amazon Alexa"
src=${brandsUrl({
domain: "alexa",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.title"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.assistants.text"
)}
</p>
</div>
<div class="feature">
<div class="logos">
<div class="round-icon">
<ha-svg-icon .path=${mdiMicrophoneMessage}></ha-svg-icon>
</div>
</div>
<h2>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.title"
)}
</h2>
<p>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.features.speech.text"
)}
</p>
</div>
</div>
<div class="more">
<a href="https://www.nabucasa.com" target="_blank" rel="noreferrer">
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.and_more"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</div>
</div>
${isComponentLoaded(this.hass, "cloud")
? html`
<div class="card-actions">
<a href="/config/cloud/register">
<ha-button unelevated>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.try_one_month"
)}
</ha-button>
</a>
<a href="/config/cloud/login">
<ha-button>
${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.cloud.sign_in"
)}
</ha-button>
</a>
</div>
`
: nothing}
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-card {
display: flex;
flex-direction: column;
}
.card-content {
padding: 24px 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.header {
font-weight: 400;
font-size: 28px;
line-height: 36px;
text-align: center;
max-width: 600px;
margin: 0 auto 8px auto;
}
@media (min-width: 800px) {
.header {
font-size: 32px;
line-height: 40px;
margin-bottom: 16px;
}
}
.features {
display: grid;
grid-template-columns: auto;
grid-gap: 16px;
padding: 16px;
}
@media (min-width: 600px) {
.features {
grid-template-columns: repeat(2, 1fr);
}
}
.feature {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: 16px;
}
.feature .logos {
margin-bottom: 16px;
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: 50%;
color: #6e41ab;
background-color: #e8dcf7;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.feature h2 {
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin-top: 0;
margin-bottom: 8px;
}
.feature p {
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin: 0;
}
.more {
display: flex;
align-items: center;
justify-content: center;
}
.more a {
text-decoration: none;
color: var(--primary-color);
font-weight: 500;
font-size: 14px;
}
.more a ha-svg-icon {
--mdc-icon-size: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"cloud-discover": CloudDiscover;
}
}

View File

@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-card";
@@ -18,10 +19,13 @@ import {
getExposeNewEntities,
setExposeNewEntities,
} from "../../../data/voice";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
export class CloudGooglePref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>;
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@state() private _exposeNew?: boolean;
@@ -36,6 +40,13 @@ export class CloudGooglePref extends LitElement {
}
}
private _exposedEntities = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.["cloud.google_assistant"]?.should_expose
).length
);
protected render() {
if (!this.cloudStatus) {
return nothing;
@@ -215,17 +226,28 @@ export class CloudGooglePref extends LitElement {
`
: ""}`}
</div>
<div class="card-actions">
<a
href="/config/voice-assistants/expose?assistants=cloud.google_assistant&historyBack"
>
<mwc-button>
${this.hass.localize(
"ui.panel.config.cloud.account.google.manage_entities"
)}
</mwc-button>
</a>
</div>
${google_enabled
? html`<div class="card-actions">
<a
href="/config/voice-assistants/expose?assistants=cloud.google_assistant&historyBack"
>
<mwc-button>
${manualConfig
? this.hass!.localize(
"ui.panel.config.cloud.account.google.show_entities"
)
: this.hass.localize(
"ui.panel.config.cloud.account.google.exposed_entities",
{
number: this.extEntities
? this._exposedEntities(this.extEntities)
: 0,
}
)}
</mwc-button>
</a>
</div>`
: nothing}
</ha-card>
`;
}

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