mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-25 09:09:26 +00:00
Compare commits
147 Commits
boolean_se
...
break-out-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
adf77e1e80 | ||
![]() |
344ff9da5b | ||
![]() |
3478bd309b | ||
![]() |
64b8b7658d | ||
![]() |
a1af8718a0 | ||
![]() |
fd9e2b647d | ||
![]() |
caee4ba7bc | ||
![]() |
915036006d | ||
![]() |
48887f2066 | ||
![]() |
68d9ce7923 | ||
![]() |
a36f3c8fb1 | ||
![]() |
4dfadea9e9 | ||
![]() |
71dc26edab | ||
![]() |
f260c95add | ||
![]() |
dc6f1efffb | ||
![]() |
b7763882f4 | ||
![]() |
7de5c46f14 | ||
![]() |
5920efa2b2 | ||
![]() |
d2194d55f9 | ||
![]() |
c0043af4c9 | ||
![]() |
dcf763438b | ||
![]() |
858a00e28c | ||
![]() |
ab407e8274 | ||
![]() |
14f96a6262 | ||
![]() |
2b33c70e04 | ||
![]() |
717443e2d6 | ||
![]() |
2aba9099a0 | ||
![]() |
3079f126a8 | ||
![]() |
1cdfb746bf | ||
![]() |
39a1844991 | ||
![]() |
9e4dc0d39e | ||
![]() |
ab91a4b814 | ||
![]() |
ca66c02fb3 | ||
![]() |
97bb052d71 | ||
![]() |
137bb473c0 | ||
![]() |
326b57f91b | ||
![]() |
32feab6a70 | ||
![]() |
68a0d04f04 | ||
![]() |
9078ab4026 | ||
![]() |
8605684906 | ||
![]() |
9f17d17d6e | ||
![]() |
ba5f176d52 | ||
![]() |
7115d14699 | ||
![]() |
23e37daff3 | ||
![]() |
ed6c2dfe39 | ||
![]() |
b48a28f2a6 | ||
![]() |
3166fec7db | ||
![]() |
1a67bd0414 | ||
![]() |
d34c43e292 | ||
![]() |
c7cfbb5b6c | ||
![]() |
bde2fd8202 | ||
![]() |
e5327c0903 | ||
![]() |
1a0ca1b78f | ||
![]() |
ed141b1d12 | ||
![]() |
5a7a71c551 | ||
![]() |
f09e0d187b | ||
![]() |
7f6325fa5e | ||
![]() |
de292a8143 | ||
![]() |
84b2005844 | ||
![]() |
0d93432a2c | ||
![]() |
8bc9927ee2 | ||
![]() |
484bed4dab | ||
![]() |
3d7e243707 | ||
![]() |
f8a432c89e | ||
![]() |
d484b2f63d | ||
![]() |
30d9186031 | ||
![]() |
cd74367acc | ||
![]() |
618cd9d9e5 | ||
![]() |
0ff2f1bf75 | ||
![]() |
d28f1f07e7 | ||
![]() |
6aa5bc2d8b | ||
![]() |
76fc0c7ab1 | ||
![]() |
7aa7019386 | ||
![]() |
b69f0964c9 | ||
![]() |
2f9b6d000b | ||
![]() |
9ff3218964 | ||
![]() |
94f186c436 | ||
![]() |
449f858ac8 | ||
![]() |
91a2f2cf24 | ||
![]() |
2c975d4f41 | ||
![]() |
ab534933fc | ||
![]() |
e353aaa339 | ||
![]() |
020904f8f6 | ||
![]() |
fa8b3f006d | ||
![]() |
d9ce20992c | ||
![]() |
a09f44dcd2 | ||
![]() |
c709059c00 | ||
![]() |
5613df1d01 | ||
![]() |
d8013a4db9 | ||
![]() |
216dbc4d41 | ||
![]() |
c40751dadd | ||
![]() |
f58d3ad670 | ||
![]() |
682f5345cc | ||
![]() |
a69771c1f8 | ||
![]() |
22de449dda | ||
![]() |
05a27b9399 | ||
![]() |
32083ea13d | ||
![]() |
18210f35b5 | ||
![]() |
362a6f46fe | ||
![]() |
1c9d411d3a | ||
![]() |
6d84523456 | ||
![]() |
2a18706a13 | ||
![]() |
87b58b0bbd | ||
![]() |
3ebb268b57 | ||
![]() |
2df097cd1b | ||
![]() |
8349e47c17 | ||
![]() |
4913932c97 | ||
![]() |
19f057a51b | ||
![]() |
c556742ff4 | ||
![]() |
8b5f731d0c | ||
![]() |
9568677926 | ||
![]() |
93ee5de1b4 | ||
![]() |
2f68ee0efc | ||
![]() |
5a229e3c88 | ||
![]() |
7c5f947865 | ||
![]() |
e9cbd54979 | ||
![]() |
cf55824899 | ||
![]() |
395586ddeb | ||
![]() |
9c48dbf232 | ||
![]() |
00eb820e36 | ||
![]() |
6b99cda982 | ||
![]() |
9bde0e876d | ||
![]() |
9482fcb04b | ||
![]() |
883ad58f52 | ||
![]() |
11ace6002a | ||
![]() |
c416daeb92 | ||
![]() |
199b7d9bc3 | ||
![]() |
a9f8eb5ab1 | ||
![]() |
ab38dad156 | ||
![]() |
1ddeca3eeb | ||
![]() |
f08f455698 | ||
![]() |
735560c552 | ||
![]() |
b5c2b555bc | ||
![]() |
b368f886f9 | ||
![]() |
5f0497a3b8 | ||
![]() |
da75eecfa5 | ||
![]() |
fdf829bc81 | ||
![]() |
0447247add | ||
![]() |
895333aa05 | ||
![]() |
58ba9f628a | ||
![]() |
28ced4bfd3 | ||
![]() |
fd6a192db1 | ||
![]() |
d72e8c35d8 | ||
![]() |
5bc3ad4c63 | ||
![]() |
530745d20d | ||
![]() |
a16cae0671 | ||
![]() |
8d0c4e4a52 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.3.6
|
uses: actions/upload-artifact@v4.4.0
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.3.6
|
uses: actions/upload-artifact@v4.4.0
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4.3.6
|
uses: actions/upload-artifact@v4.4.0
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.3.6
|
uses: actions/upload-artifact@v4.4.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
@@ -1,16 +1,7 @@
|
|||||||
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
|
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
|
||||||
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
|
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
|
||||||
--- a/modular/sortable.core.esm.js
|
--- a/modular/sortable.core.esm.js
|
||||||
+++ b/modular/sortable.core.esm.js
|
+++ b/modular/sortable.core.esm.js
|
||||||
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
|
||||||
}
|
|
||||||
target = parent; // store last element
|
|
||||||
}
|
|
||||||
- /* jshint boss:true */ while (parent = parent.parentNode);
|
|
||||||
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
|
|
||||||
}
|
|
||||||
_unhideGhostForTarget();
|
|
||||||
}
|
|
||||||
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||||
}
|
}
|
||||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
|
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
|
||||||
@@ -33,7 +24,7 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
|
|||||||
}
|
}
|
||||||
parentEl = el; // actualization
|
parentEl = el; // actualization
|
||||||
|
|
||||||
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||||
targetRect = getRect(target);
|
targetRect = getRect(target);
|
||||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
|
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
|
||||||
capture();
|
capture();
|
||||||
@@ -44,11 +35,10 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
|
|||||||
+ catch(err) {
|
+ catch(err) {
|
||||||
+ return completed(false);
|
+ return completed(false);
|
||||||
+ }
|
+ }
|
||||||
+
|
|
||||||
parentEl = el; // actualization
|
parentEl = el; // actualization
|
||||||
|
|
||||||
changed();
|
changed();
|
||||||
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||||
_silent = true;
|
_silent = true;
|
||||||
setTimeout(_unsilent, 30);
|
setTimeout(_unsilent, 30);
|
||||||
capture();
|
capture();
|
||||||
@@ -56,8 +46,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
|
|||||||
- el.appendChild(dragEl);
|
- el.appendChild(dragEl);
|
||||||
- } else {
|
- } else {
|
||||||
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
||||||
- }
|
|
||||||
|
|
||||||
+ try {
|
+ try {
|
||||||
+ if (after && !nextSibling) {
|
+ if (after && !nextSibling) {
|
||||||
+ el.appendChild(dragEl);
|
+ el.appendChild(dragEl);
|
||||||
@@ -67,7 +55,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
|
|||||||
+ }
|
+ }
|
||||||
+ catch(err) {
|
+ catch(err) {
|
||||||
+ return completed(false);
|
+ return completed(false);
|
||||||
+ }
|
}
|
||||||
|
|
||||||
// Undo chrome's scroll adjustment (has no effect on other browsers)
|
// Undo chrome's scroll adjustment (has no effect on other browsers)
|
||||||
if (scrolledPastTop) {
|
|
||||||
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);
|
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
|
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||||
|
@@ -15,23 +15,29 @@ const brotliOptions = {
|
|||||||
};
|
};
|
||||||
const zopfliOptions = { threshold: 150 };
|
const zopfliOptions = { threshold: 150 };
|
||||||
|
|
||||||
const compressDistBrotli = (rootDir, modernDir) =>
|
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
|
||||||
gulp
|
gulp
|
||||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
.src(
|
||||||
base: rootDir,
|
[
|
||||||
})
|
`${modernDir}/**/${filesGlob}`,
|
||||||
|
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
|
||||||
|
].filter(Boolean),
|
||||||
|
{
|
||||||
|
base: rootDir,
|
||||||
|
}
|
||||||
|
)
|
||||||
.pipe(brotli(brotliOptions))
|
.pipe(brotli(brotliOptions))
|
||||||
.pipe(gulp.dest(rootDir));
|
.pipe(gulp.dest(rootDir));
|
||||||
|
|
||||||
const compressDistZopfli = (rootDir, modernDir) =>
|
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
|
||||||
gulp
|
gulp
|
||||||
.src(
|
.src(
|
||||||
[
|
[
|
||||||
`${rootDir}/**/${filesGlob}`,
|
`${rootDir}/**/${filesGlob}`,
|
||||||
`!${modernDir}/**/${filesGlob}`,
|
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
|
||||||
`!${rootDir}/{sw-modern,service_worker}.js`,
|
`!${rootDir}/{sw-modern,service_worker}.js`,
|
||||||
`${rootDir}/{authorize,onboarding}.html`,
|
`${rootDir}/{authorize,onboarding}.html`,
|
||||||
],
|
].filter(Boolean),
|
||||||
{ base: rootDir }
|
{ base: rootDir }
|
||||||
)
|
)
|
||||||
.pipe(zopfli(zopfliOptions))
|
.pipe(zopfli(zopfliOptions))
|
||||||
@@ -40,12 +46,20 @@ const compressDistZopfli = (rootDir, modernDir) =>
|
|||||||
const compressAppBrotli = () =>
|
const compressAppBrotli = () =>
|
||||||
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
|
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
|
||||||
const compressHassioBrotli = () =>
|
const compressHassioBrotli = () =>
|
||||||
compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest);
|
compressDistBrotli(
|
||||||
|
paths.hassio_output_root,
|
||||||
|
paths.hassio_output_latest,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
const compressAppZopfli = () =>
|
const compressAppZopfli = () =>
|
||||||
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
|
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
|
||||||
const compressHassioZopfli = () =>
|
const compressHassioZopfli = () =>
|
||||||
compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest);
|
compressDistZopfli(
|
||||||
|
paths.hassio_output_root,
|
||||||
|
paths.hassio_output_latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
|
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
|
||||||
gulp.task(
|
gulp.task(
|
||||||
|
@@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
|
|||||||
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
|
||||||
staticPath("polyfills/")
|
staticPath("polyfills/")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// dialog-polyfill css
|
||||||
|
copyFileDir(
|
||||||
|
npmPath("dialog-polyfill/dialog-polyfill.css"),
|
||||||
|
staticPath("polyfills/")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyLoaderJS(staticDir) {
|
function copyLoaderJS(staticDir) {
|
||||||
|
@@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
|
|||||||
friendly_name: "Living room Temperature",
|
friendly_name: "Living room Temperature",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"sensor.outdoor_temperature": {
|
||||||
|
entity_id: "sensor.outdoor_temperature",
|
||||||
|
state: "10.5",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
unit_of_measurement: "°C",
|
||||||
|
device_class: "temperature",
|
||||||
|
friendly_name: "Outdoor temperature",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.outdoor_humidity": {
|
||||||
|
entity_id: "sensor.outdoor_humidity",
|
||||||
|
state: "70.4",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
unit_of_measurement: "%",
|
||||||
|
device_class: "humidity",
|
||||||
|
friendly_name: "Outdoor humidity",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"device_tracker.car": {
|
||||||
|
entity_id: "sensor.outdoor_humidity",
|
||||||
|
state: "not_home",
|
||||||
|
attributes: {
|
||||||
|
friendly_name: "Car",
|
||||||
|
icon: "mdi:car",
|
||||||
|
},
|
||||||
|
},
|
||||||
"media_player.living_room_nest_mini": {
|
"media_player.living_room_nest_mini": {
|
||||||
entity_id: "media_player.living_room_nest_mini",
|
entity_id: "media_player.living_room_nest_mini",
|
||||||
state: "on",
|
state: "playing",
|
||||||
attributes: {
|
attributes: {
|
||||||
device_class: "speaker",
|
device_class: "speaker",
|
||||||
volume_level: 0.18,
|
volume_level: 0.18,
|
||||||
|
@@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
|
|||||||
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
|
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
|
||||||
path: "home",
|
path: "home",
|
||||||
icon: "mdi:home-assistant",
|
icon: "mdi:home-assistant",
|
||||||
|
badges: [
|
||||||
|
{
|
||||||
|
type: "entity",
|
||||||
|
entity: "sensor.outdoor_temperature",
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "entity",
|
||||||
|
entity: "sensor.outdoor_humidity",
|
||||||
|
color: "indigo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "entity",
|
||||||
|
entity: "device_tracker.car",
|
||||||
|
},
|
||||||
|
],
|
||||||
sections: [
|
sections: [
|
||||||
...(isFrontpageEmbed
|
...(isFrontpageEmbed
|
||||||
? []
|
? []
|
||||||
|
@@ -232,7 +232,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
],
|
],
|
||||||
action: [
|
action: [
|
||||||
{
|
{
|
||||||
service: "input_boolean.toggle",
|
action: "input_boolean.toggle",
|
||||||
target: {
|
target: {
|
||||||
entity_id: "input_boolean.toggle_4",
|
entity_id: "input_boolean.toggle_4",
|
||||||
},
|
},
|
||||||
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
],
|
],
|
||||||
default: [
|
default: [
|
||||||
{
|
{
|
||||||
service: "input_boolean.toggle",
|
action: "input_boolean.toggle",
|
||||||
alias: "Toggle 2",
|
alias: "Toggle 2",
|
||||||
target: {
|
target: {
|
||||||
entity_id: "input_boolean.toggle_2",
|
entity_id: "input_boolean.toggle_2",
|
||||||
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: "input_boolean.toggle",
|
action: "input_boolean.toggle",
|
||||||
target: {
|
target: {
|
||||||
entity_id: "input_boolean.toggle_4",
|
entity_id: "input_boolean.toggle_4",
|
||||||
},
|
},
|
||||||
|
@@ -143,7 +143,7 @@ export const motionLightTrace: DemoTrace = {
|
|||||||
],
|
],
|
||||||
action: [
|
action: [
|
||||||
{
|
{
|
||||||
service: "light.turn_on",
|
action: "light.turn_on",
|
||||||
target: {
|
target: {
|
||||||
entity_id: "light.elgato_key_light_air",
|
entity_id: "light.elgato_key_light_air",
|
||||||
},
|
},
|
||||||
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
|
|||||||
delay: 0,
|
delay: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: "light.turn_off",
|
action: "light.turn_off",
|
||||||
target: {
|
target: {
|
||||||
entity_id: "light.elgato_key_light_air",
|
entity_id: "light.elgato_key_light_air",
|
||||||
},
|
},
|
||||||
|
@@ -11,7 +11,6 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
|||||||
import type { ConditionWithShorthand } from "../../../../src/data/automation";
|
import type { ConditionWithShorthand } from "../../../../src/data/automation";
|
||||||
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
|
||||||
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
|
||||||
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
|
|
||||||
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
|
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
|
||||||
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
|
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
|
||||||
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
|
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
|
||||||
@@ -19,62 +18,67 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co
|
|||||||
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
|
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
|
||||||
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
|
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
|
||||||
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
|
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
|
||||||
|
import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and";
|
||||||
|
import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or";
|
||||||
|
import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not";
|
||||||
|
|
||||||
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
||||||
{
|
{
|
||||||
name: "State",
|
name: "State",
|
||||||
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
|
conditions: [{ ...HaStateCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Numeric State",
|
name: "Numeric State",
|
||||||
conditions: [
|
conditions: [{ ...HaNumericStateCondition.defaultConfig }],
|
||||||
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sun",
|
name: "Sun",
|
||||||
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
|
conditions: [{ ...HaSunCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Zone",
|
name: "Zone",
|
||||||
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
|
conditions: [{ ...HaZoneCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Time",
|
name: "Time",
|
||||||
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
|
conditions: [{ ...HaTimeCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Template",
|
name: "Template",
|
||||||
conditions: [
|
conditions: [{ ...HaTemplateCondition.defaultConfig }],
|
||||||
{ condition: "template", ...HaTemplateCondition.defaultConfig },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Device",
|
name: "Device",
|
||||||
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
|
conditions: [{ ...HaDeviceCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "And",
|
name: "And",
|
||||||
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
|
conditions: [{ ...HaAndCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Or",
|
name: "Or",
|
||||||
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
|
conditions: [{ ...HaOrCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not",
|
name: "Not",
|
||||||
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
|
conditions: [{ ...HaNotCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Trigger",
|
name: "Trigger",
|
||||||
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
|
conditions: [{ ...HaTriggerCondition.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Shorthand",
|
name: "Shorthand",
|
||||||
conditions: [
|
conditions: [
|
||||||
{ and: HaLogicalCondition.defaultConfig.conditions },
|
{
|
||||||
{ or: HaLogicalCondition.defaultConfig.conditions },
|
...HaAndCondition.defaultConfig,
|
||||||
{ not: HaLogicalCondition.defaultConfig.conditions },
|
},
|
||||||
|
{
|
||||||
|
...HaOrCondition.defaultConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...HaNotCondition.defaultConfig,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@@ -30,55 +30,48 @@ import { HaConversationTrigger } from "../../../../src/panels/config/automation/
|
|||||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||||
{
|
{
|
||||||
name: "State",
|
name: "State",
|
||||||
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
|
triggers: [{ ...HaStateTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "MQTT",
|
name: "MQTT",
|
||||||
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
|
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "GeoLocation",
|
name: "GeoLocation",
|
||||||
triggers: [
|
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
|
||||||
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Home Assistant",
|
name: "Home Assistant",
|
||||||
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
|
triggers: [{ ...HaHassTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Numeric State",
|
name: "Numeric State",
|
||||||
triggers: [
|
triggers: [{ ...HaNumericStateTrigger.defaultConfig }],
|
||||||
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Sun",
|
name: "Sun",
|
||||||
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
|
triggers: [{ ...HaSunTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Time Pattern",
|
name: "Time Pattern",
|
||||||
triggers: [
|
triggers: [{ ...HaTimePatternTrigger.defaultConfig }],
|
||||||
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Webhook",
|
name: "Webhook",
|
||||||
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
|
triggers: [{ ...HaWebhookTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Persistent Notification",
|
name: "Persistent Notification",
|
||||||
triggers: [
|
triggers: [
|
||||||
{
|
{
|
||||||
platform: "persistent_notification",
|
|
||||||
...HaPersistentNotificationTrigger.defaultConfig,
|
...HaPersistentNotificationTrigger.defaultConfig,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -86,37 +79,37 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "Zone",
|
name: "Zone",
|
||||||
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
|
triggers: [{ ...HaZoneTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Tag",
|
name: "Tag",
|
||||||
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
|
triggers: [{ ...HaTagTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Time",
|
name: "Time",
|
||||||
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
|
triggers: [{ ...HaTimeTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Template",
|
name: "Template",
|
||||||
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
|
triggers: [{ ...HaTemplateTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Event",
|
name: "Event",
|
||||||
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
|
triggers: [{ ...HaEventTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "Device Trigger",
|
name: "Device Trigger",
|
||||||
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
triggers: [{ ...HaDeviceTrigger.defaultConfig }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Sentence",
|
name: "Sentence",
|
||||||
triggers: [
|
triggers: [
|
||||||
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
|
{ ...HaConversationTrigger.defaultConfig },
|
||||||
{
|
{
|
||||||
platform: "conversation",
|
platform: "conversation",
|
||||||
command: ["Turn on the lights", "Turn the lights on"],
|
command: ["Turn on the lights", "Turn the lights on"],
|
||||||
|
@@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: "backyard",
|
area_id: "backyard",
|
||||||
@@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area_id: null,
|
area_id: null,
|
||||||
@@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
3
gallery/src/pages/misc/ha-markdown.markdown
Normal file
3
gallery/src/pages/misc/ha-markdown.markdown
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Markdown
|
||||||
|
---
|
93
gallery/src/pages/misc/ha-markdown.ts
Normal file
93
gallery/src/pages/misc/ha-markdown.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import "../../../../src/components/ha-card";
|
||||||
|
import "../../../../src/components/ha-markdown";
|
||||||
|
|
||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
interface MarkdownContent {
|
||||||
|
content: string;
|
||||||
|
breaks: boolean;
|
||||||
|
allowSvg: boolean;
|
||||||
|
lazyImages: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdContentwithDefaults = (md: Partial<MarkdownContent>) =>
|
||||||
|
({
|
||||||
|
breaks: false,
|
||||||
|
allowSvg: false,
|
||||||
|
lazyImages: false,
|
||||||
|
...md,
|
||||||
|
}) as MarkdownContent;
|
||||||
|
|
||||||
|
const generateContent = (md) => `
|
||||||
|
\`\`\`json
|
||||||
|
${JSON.stringify({ ...md, content: undefined })}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${md.content}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const markdownContents: MarkdownContent[] = [
|
||||||
|
mdContentwithDefaults({
|
||||||
|
content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.",
|
||||||
|
}),
|
||||||
|
...[true, false].map((breaks) =>
|
||||||
|
mdContentwithDefaults({
|
||||||
|
breaks,
|
||||||
|
content: `
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non.
|
||||||
|
|
||||||
|
key | description
|
||||||
|
-- | --
|
||||||
|
lorem | ipsum
|
||||||
|
|
||||||
|
- list item 1
|
||||||
|
- list item 2
|
||||||
|
|
||||||
|
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@customElement("demo-misc-ha-markdown")
|
||||||
|
export class DemoMiscMarkdown extends LitElement {
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
${markdownContents.map(
|
||||||
|
(md) =>
|
||||||
|
html`<ha-card>
|
||||||
|
<ha-markdown
|
||||||
|
.content=${generateContent(md)}
|
||||||
|
.breaks=${md.breaks}
|
||||||
|
.allowSvg=${md.allowSvg}
|
||||||
|
.lazyImages=${md.lazyImages}
|
||||||
|
></ha-markdown>
|
||||||
|
</ha-card>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
margin: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-misc-ha-markdown": DemoMiscMarkdown;
|
||||||
|
}
|
||||||
|
}
|
@@ -232,6 +232,7 @@ const createDeviceRegistryEntries = (
|
|||||||
labels: [],
|
labels: [],
|
||||||
created_at: 0,
|
created_at: 0,
|
||||||
modified_at: 0,
|
modified_at: 0,
|
||||||
|
primary_config_entry: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
|
|||||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||||
import "../../../../src/components/ha-textfield";
|
import "../../../../src/components/ha-textfield";
|
||||||
import "../../../../src/components/ha-list-new";
|
import "../../../../src/components/ha-md-list";
|
||||||
import "../../../../src/components/ha-list-item-new";
|
import "../../../../src/components/ha-md-list-item";
|
||||||
|
|
||||||
@customElement("dialog-hassio-repositories")
|
@customElement("dialog-hassio-repositories")
|
||||||
class HassioRepositoriesDialog extends LitElement {
|
class HassioRepositoriesDialog extends LitElement {
|
||||||
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<ha-list-new>
|
<ha-md-list>
|
||||||
${repositories.length
|
${repositories.length
|
||||||
? repositories.map(
|
? repositories.map(
|
||||||
(repo) => html`
|
(repo) => html`
|
||||||
<ha-list-item-new class="option">
|
<ha-md-list-item class="option">
|
||||||
${repo.name}
|
${repo.name}
|
||||||
<div slot="supporting-text">
|
<div slot="supporting-text">
|
||||||
<div>${repo.maintainer}</div>
|
<div>${repo.maintainer}</div>
|
||||||
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</simple-tooltip>
|
</simple-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</ha-list-item-new>
|
</ha-md-list-item>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
|
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
|
||||||
</ha-list-new>
|
</ha-md-list>
|
||||||
<div class="layout horizontal bottom">
|
<div class="layout horizontal bottom">
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="flex-auto"
|
class="flex-auto"
|
||||||
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
|
|||||||
div.delete ha-icon-button {
|
div.delete ha-icon-button {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
}
|
}
|
||||||
ha-list-item-new {
|
ha-md-list-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
51
package.json
51
package.json
@@ -25,15 +25,15 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.25.4",
|
"@babel/runtime": "7.25.6",
|
||||||
"@braintree/sanitize-url": "7.1.0",
|
"@braintree/sanitize-url": "7.1.0",
|
||||||
"@codemirror/autocomplete": "6.18.0",
|
"@codemirror/autocomplete": "6.18.1",
|
||||||
"@codemirror/commands": "6.6.0",
|
"@codemirror/commands": "6.6.2",
|
||||||
"@codemirror/language": "6.10.2",
|
"@codemirror/language": "6.10.3",
|
||||||
"@codemirror/legacy-modes": "6.4.1",
|
"@codemirror/legacy-modes": "6.4.1",
|
||||||
"@codemirror/search": "6.5.6",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "6.32.0",
|
"@codemirror/view": "6.33.0",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||||
"@formatjs/intl-displaynames": "6.6.8",
|
"@formatjs/intl-displaynames": "6.6.8",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "2.1.0",
|
"@material/web": "2.2.0",
|
||||||
"@mdi/js": "7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
"@mdi/svg": "7.4.47",
|
"@mdi/svg": "7.4.47",
|
||||||
"@polymer/paper-item": "3.0.1",
|
"@polymer/paper-item": "3.0.1",
|
||||||
@@ -88,8 +88,8 @@
|
|||||||
"@polymer/paper-tabs": "3.1.0",
|
"@polymer/paper-tabs": "3.1.0",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.4.6",
|
"@vaadin/combo-box": "24.4.9",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.4.6",
|
"@vaadin/vaadin-themable-mixin": "24.4.9",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@@ -102,10 +102,11 @@
|
|||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.38.1",
|
"core-js": "3.38.1",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "4.1.0",
|
||||||
"date-fns-tz": "3.1.3",
|
"date-fns-tz": "3.1.3",
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
|
"dialog-polyfill": "0.5.6",
|
||||||
"element-internals-polyfill": "1.3.11",
|
"element-internals-polyfill": "1.3.11",
|
||||||
"fuse.js": "7.0.0",
|
"fuse.js": "7.0.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
"lit": "2.8.0",
|
"lit": "2.8.0",
|
||||||
"luxon": "3.5.0",
|
"luxon": "3.5.0",
|
||||||
"marked": "14.0.0",
|
"marked": "14.1.2",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "0.3.2",
|
"proxy-polyfill": "0.3.2",
|
||||||
@@ -127,13 +128,13 @@
|
|||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"roboto-fontface": "0.10.0",
|
"roboto-fontface": "0.10.0",
|
||||||
"rrule": "2.8.1",
|
"rrule": "2.8.1",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.3",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"tsparticles-engine": "2.12.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.12.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
"ua-parser-js": "1.0.38",
|
"ua-parser-js": "1.0.39",
|
||||||
"unfetch": "5.0.0",
|
"unfetch": "5.0.0",
|
||||||
"vis-data": "7.1.9",
|
"vis-data": "7.1.9",
|
||||||
"vis-network": "9.1.9",
|
"vis-network": "9.1.9",
|
||||||
@@ -155,7 +156,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "7.25.4",
|
"@babel/plugin-transform-runtime": "7.25.4",
|
||||||
"@babel/preset-env": "7.25.4",
|
"@babel/preset-env": "7.25.4",
|
||||||
"@babel/preset-typescript": "7.24.7",
|
"@babel/preset-typescript": "7.24.7",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.14.2",
|
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||||
"@koa/cors": "5.0.0",
|
"@koa/cors": "5.0.0",
|
||||||
"@lokalise/node-api": "12.7.0",
|
"@lokalise/node-api": "12.7.0",
|
||||||
"@octokit/auth-oauth-device": "7.1.1",
|
"@octokit/auth-oauth-device": "7.1.1",
|
||||||
@@ -189,7 +190,7 @@
|
|||||||
"@typescript-eslint/parser": "7.18.0",
|
"@typescript-eslint/parser": "7.18.0",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.2.1",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"chai": "5.1.1",
|
"chai": "5.1.1",
|
||||||
@@ -198,11 +199,11 @@
|
|||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "18.0.0",
|
"eslint-config-airbnb-typescript": "18.0.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-webpack": "0.13.8",
|
"eslint-import-resolver-webpack": "0.13.9",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.30.0",
|
||||||
"eslint-plugin-lit": "1.14.0",
|
"eslint-plugin-lit": "1.15.0",
|
||||||
"eslint-plugin-lit-a11y": "4.1.4",
|
"eslint-plugin-lit-a11y": "4.1.4",
|
||||||
"eslint-plugin-unused-imports": "4.1.3",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"eslint-plugin-wc": "2.1.1",
|
"eslint-plugin-wc": "2.1.1",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
@@ -213,10 +214,10 @@
|
|||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.5",
|
"husky": "9.1.6",
|
||||||
"instant-mocha": "1.5.2",
|
"instant-mocha": "1.5.2",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "15.2.9",
|
"lint-staged": "15.2.10",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
@@ -232,16 +233,16 @@
|
|||||||
"rollup-plugin-terser": "7.0.2",
|
"rollup-plugin-terser": "7.0.2",
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
"rollup-plugin-visualizer": "5.12.0",
|
||||||
"serve-handler": "6.1.5",
|
"serve-handler": "6.1.5",
|
||||||
"sinon": "18.0.0",
|
"sinon": "19.0.2",
|
||||||
"systemjs": "6.15.1",
|
"systemjs": "6.15.1",
|
||||||
"tar": "7.4.3",
|
"tar": "7.4.3",
|
||||||
"terser-webpack-plugin": "5.3.10",
|
"terser-webpack-plugin": "5.3.10",
|
||||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.6.2",
|
||||||
"webpack": "5.94.0",
|
"webpack": "5.94.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "5.0.4",
|
"webpack-dev-server": "5.1.0",
|
||||||
"webpack-manifest-plugin": "5.0.0",
|
"webpack-manifest-plugin": "5.0.0",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "6.0.1",
|
"webpackbar": "6.0.1",
|
||||||
@@ -255,8 +256,8 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "1.6.3",
|
"@lit/reactive-element": "1.6.3",
|
||||||
"@fullcalendar/daygrid": "6.1.15",
|
"@fullcalendar/daygrid": "6.1.15",
|
||||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
"sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
|
||||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.4.0"
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20240809.0"
|
version = "20240909.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
|
|||||||
"weather",
|
"weather",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
|
export const ASSIST_ENTITIES = [
|
||||||
|
"assist_satellite",
|
||||||
|
"conversation",
|
||||||
|
"stt",
|
||||||
|
"tts",
|
||||||
|
];
|
||||||
|
|
||||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||||
|
@@ -71,8 +71,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
if (
|
if (
|
||||||
attributes.device_class === "duration" &&
|
attributes.device_class === "duration" &&
|
||||||
attributes.unit_of_measurement &&
|
attributes.unit_of_measurement &&
|
||||||
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] &&
|
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
|
||||||
entity?.display_precision === undefined
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return formatDuration(state, attributes.unit_of_measurement);
|
return formatDuration(state, attributes.unit_of_measurement);
|
||||||
|
6
src/components/chart/click_is_touch.ts
Normal file
6
src/components/chart/click_is_touch.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { ChartEvent } from "chart.js";
|
||||||
|
|
||||||
|
export const clickIsTouch = (event: ChartEvent): boolean =>
|
||||||
|
!(event.native instanceof MouseEvent) ||
|
||||||
|
(event.native instanceof PointerEvent &&
|
||||||
|
event.native.pointerType !== "mouse");
|
@@ -16,6 +16,7 @@ import {
|
|||||||
HaChartBase,
|
HaChartBase,
|
||||||
MIN_TIME_BETWEEN_UPDATES,
|
MIN_TIME_BETWEEN_UPDATES,
|
||||||
} from "./ha-chart-base";
|
} from "./ha-chart-base";
|
||||||
|
import { clickIsTouch } from "./click_is_touch";
|
||||||
|
|
||||||
const safeParseFloat = (value) => {
|
const safeParseFloat = (value) => {
|
||||||
const parsed = parseFloat(value);
|
const parsed = parseFloat(value);
|
||||||
@@ -220,12 +221,7 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(this.hass.locale),
|
locale: numberFormatToLocale(this.hass.locale),
|
||||||
onClick: (e: any) => {
|
onClick: (e: any) => {
|
||||||
if (
|
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||||
!this.clickForMoreInfo ||
|
|
||||||
!(e.native instanceof MouseEvent) ||
|
|
||||||
(e.native instanceof PointerEvent &&
|
|
||||||
e.native.pointerType !== "mouse")
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "./ha-chart-base";
|
} from "./ha-chart-base";
|
||||||
import type { TimeLineData } from "./timeline-chart/const";
|
import type { TimeLineData } from "./timeline-chart/const";
|
||||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||||
|
import { clickIsTouch } from "./click_is_touch";
|
||||||
|
|
||||||
@customElement("state-history-chart-timeline")
|
@customElement("state-history-chart-timeline")
|
||||||
export class StateHistoryChartTimeline extends LitElement {
|
export class StateHistoryChartTimeline extends LitElement {
|
||||||
@@ -224,11 +225,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(this.hass.locale),
|
locale: numberFormatToLocale(this.hass.locale),
|
||||||
onClick: (e: any) => {
|
onClick: (e: any) => {
|
||||||
if (
|
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||||
!this.clickForMoreInfo ||
|
|
||||||
!(e.native instanceof MouseEvent) ||
|
|
||||||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -39,6 +39,7 @@ import type {
|
|||||||
ChartDatasetExtra,
|
ChartDatasetExtra,
|
||||||
HaChartBase,
|
HaChartBase,
|
||||||
} from "./ha-chart-base";
|
} from "./ha-chart-base";
|
||||||
|
import { clickIsTouch } from "./click_is_touch";
|
||||||
|
|
||||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||||
mean: "mean",
|
mean: "mean",
|
||||||
@@ -278,11 +279,7 @@ export class StatisticsChart extends LitElement {
|
|||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
locale: numberFormatToLocale(this.hass.locale),
|
locale: numberFormatToLocale(this.hass.locale),
|
||||||
onClick: (e: any) => {
|
onClick: (e: any) => {
|
||||||
if (
|
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||||
!this.clickForMoreInfo ||
|
|
||||||
!(e.native instanceof MouseEvent) ||
|
|
||||||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,7 +25,6 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
import { groupBy } from "../../common/util/group-by";
|
||||||
import { nextRender } from "../../common/util/render-status";
|
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
import { haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
@@ -35,6 +34,7 @@ import "../ha-svg-icon";
|
|||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
import { LocalizeFunc } from "../../common/translations/localize";
|
import { LocalizeFunc } from "../../common/translations/localize";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
|
|
||||||
export interface RowClickedEvent {
|
export interface RowClickedEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -169,8 +169,6 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||||
|
|
||||||
@state() private _items: DataTableRowData[] = [];
|
|
||||||
|
|
||||||
@state() private _collapsedGroups: string[] = [];
|
@state() private _collapsedGroups: string[] = [];
|
||||||
|
|
||||||
private _checkableRowsCount?: number;
|
private _checkableRowsCount?: number;
|
||||||
@@ -179,7 +177,9 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
private _sortColumns: SortableColumnContainer = {};
|
private _sortColumns: SortableColumnContainer = {};
|
||||||
|
|
||||||
private curRequest = 0;
|
private _curRequest = 0;
|
||||||
|
|
||||||
|
private _lastUpdate = 0;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@restoreScroll(".scroller") private _savedScrollPos?: number;
|
@restoreScroll(".scroller") private _savedScrollPos?: number;
|
||||||
@@ -206,9 +206,9 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this._items.length) {
|
if (this._filteredData.length) {
|
||||||
// Force update of location of rows
|
// Force update of location of rows
|
||||||
this._items = [...this._items];
|
this._filteredData = [...this._filteredData];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,16 +291,13 @@ export class HaDataTable extends LitElement {
|
|||||||
properties.has("columns") ||
|
properties.has("columns") ||
|
||||||
properties.has("_filter") ||
|
properties.has("_filter") ||
|
||||||
properties.has("sortColumn") ||
|
properties.has("sortColumn") ||
|
||||||
properties.has("sortDirection") ||
|
properties.has("sortDirection")
|
||||||
properties.has("groupColumn") ||
|
|
||||||
properties.has("groupOrder") ||
|
|
||||||
properties.has("_collapsedGroups")
|
|
||||||
) {
|
) {
|
||||||
this._sortFilterData();
|
this._sortFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
||||||
this._items = [...this._items];
|
this._filteredData = [...this._filteredData];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +464,15 @@ export class HaDataTable extends LitElement {
|
|||||||
scroller
|
scroller
|
||||||
class="mdc-data-table__content scroller ha-scrollbar"
|
class="mdc-data-table__content scroller ha-scrollbar"
|
||||||
@scroll=${this._saveScrollPos}
|
@scroll=${this._saveScrollPos}
|
||||||
.items=${this._items}
|
.items=${this._groupData(
|
||||||
|
this._filteredData,
|
||||||
|
localize,
|
||||||
|
this.appendRow,
|
||||||
|
this.hasFab,
|
||||||
|
this.groupColumn,
|
||||||
|
this.groupOrder,
|
||||||
|
this._collapsedGroups
|
||||||
|
)}
|
||||||
.keyFunction=${this._keyFunction}
|
.keyFunction=${this._keyFunction}
|
||||||
.renderItem=${renderRow}
|
.renderItem=${renderRow}
|
||||||
></lit-virtualizer>
|
></lit-virtualizer>
|
||||||
@@ -602,8 +607,13 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
private async _sortFilterData() {
|
private async _sortFilterData() {
|
||||||
const startTime = new Date().getTime();
|
const startTime = new Date().getTime();
|
||||||
this.curRequest++;
|
const timeBetweenUpdate = startTime - this._lastUpdate;
|
||||||
const curRequest = this.curRequest;
|
const timeBetweenRequest = startTime - this._curRequest;
|
||||||
|
this._curRequest = startTime;
|
||||||
|
|
||||||
|
const forceUpdate =
|
||||||
|
!this._lastUpdate ||
|
||||||
|
(timeBetweenUpdate > 500 && timeBetweenRequest < 500);
|
||||||
|
|
||||||
let filteredData = this.data;
|
let filteredData = this.data;
|
||||||
if (this._filter) {
|
if (this._filter) {
|
||||||
@@ -614,6 +624,10 @@ export class HaDataTable extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!forceUpdate && this._curRequest !== startTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const prom = this.sortColumn
|
const prom = this.sortColumn
|
||||||
? sortData(
|
? sortData(
|
||||||
filteredData,
|
filteredData,
|
||||||
@@ -634,91 +648,103 @@ export class HaDataTable extends LitElement {
|
|||||||
setTimeout(resolve, 100 - elapsed);
|
setTimeout(resolve, 100 - elapsed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.curRequest !== curRequest) {
|
|
||||||
|
if (!forceUpdate && this._curRequest !== startTime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localize = this.localizeFunc || this.hass.localize;
|
this._lastUpdate = startTime;
|
||||||
|
|
||||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
|
||||||
let items = [...data];
|
|
||||||
|
|
||||||
if (this.groupColumn) {
|
|
||||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
|
||||||
if (grouped.undefined) {
|
|
||||||
// make sure ungrouped items are at the bottom
|
|
||||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
|
||||||
delete grouped.undefined;
|
|
||||||
}
|
|
||||||
const sorted: {
|
|
||||||
[key: string]: DataTableRowData[];
|
|
||||||
} = Object.keys(grouped)
|
|
||||||
.sort((a, b) => {
|
|
||||||
const orderA = this.groupOrder?.indexOf(a) ?? -1;
|
|
||||||
const orderB = this.groupOrder?.indexOf(b) ?? -1;
|
|
||||||
if (orderA !== orderB) {
|
|
||||||
if (orderA === -1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (orderB === -1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return orderA - orderB;
|
|
||||||
}
|
|
||||||
return stringCompare(
|
|
||||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
|
||||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce((obj, key) => {
|
|
||||||
obj[key] = grouped[key];
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
const groupedItems: DataTableRowData[] = [];
|
|
||||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
|
||||||
groupedItems.push({
|
|
||||||
append: true,
|
|
||||||
content: html`<div
|
|
||||||
class="mdc-data-table__cell group-header"
|
|
||||||
role="cell"
|
|
||||||
.group=${groupName}
|
|
||||||
@click=${this._collapseGroup}
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiChevronUp}
|
|
||||||
class=${this._collapsedGroups.includes(groupName)
|
|
||||||
? "collapsed"
|
|
||||||
: ""}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
${groupName === UNDEFINED_GROUP_KEY
|
|
||||||
? localize("ui.components.data-table.ungrouped")
|
|
||||||
: groupName || ""}
|
|
||||||
</div>`,
|
|
||||||
});
|
|
||||||
if (!this._collapsedGroups.includes(groupName)) {
|
|
||||||
groupedItems.push(...rows);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
items = groupedItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appendRow) {
|
|
||||||
items.push({ append: true, content: this.appendRow });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasFab) {
|
|
||||||
items.push({ empty: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this._items = items;
|
|
||||||
} else {
|
|
||||||
this._items = data;
|
|
||||||
}
|
|
||||||
this._filteredData = data;
|
this._filteredData = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _groupData = memoizeOne(
|
||||||
|
(
|
||||||
|
data: DataTableRowData[],
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
appendRow,
|
||||||
|
hasFab: boolean,
|
||||||
|
groupColumn: string | undefined,
|
||||||
|
groupOrder: string[] | undefined,
|
||||||
|
collapsedGroups: string[]
|
||||||
|
) => {
|
||||||
|
if (appendRow || hasFab || groupColumn) {
|
||||||
|
let items = [...data];
|
||||||
|
|
||||||
|
if (groupColumn) {
|
||||||
|
const grouped = groupBy(items, (item) => item[groupColumn]);
|
||||||
|
if (grouped.undefined) {
|
||||||
|
// make sure ungrouped items are at the bottom
|
||||||
|
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||||
|
delete grouped.undefined;
|
||||||
|
}
|
||||||
|
const sorted: {
|
||||||
|
[key: string]: DataTableRowData[];
|
||||||
|
} = Object.keys(grouped)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const orderA = groupOrder?.indexOf(a) ?? -1;
|
||||||
|
const orderB = groupOrder?.indexOf(b) ?? -1;
|
||||||
|
if (orderA !== orderB) {
|
||||||
|
if (orderA === -1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (orderB === -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return orderA - orderB;
|
||||||
|
}
|
||||||
|
return stringCompare(
|
||||||
|
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||||
|
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||||
|
this.hass.locale.language
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = grouped[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
const groupedItems: DataTableRowData[] = [];
|
||||||
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
|
groupedItems.push({
|
||||||
|
append: true,
|
||||||
|
content: html`<div
|
||||||
|
class="mdc-data-table__cell group-header"
|
||||||
|
role="cell"
|
||||||
|
.group=${groupName}
|
||||||
|
@click=${this._collapseGroup}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiChevronUp}
|
||||||
|
class=${collapsedGroups.includes(groupName)
|
||||||
|
? "collapsed"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
${groupName === UNDEFINED_GROUP_KEY
|
||||||
|
? localize("ui.components.data-table.ungrouped")
|
||||||
|
: groupName || ""}
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
if (!collapsedGroups.includes(groupName)) {
|
||||||
|
groupedItems.push(...rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
items = groupedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appendRow) {
|
||||||
|
items.push({ append: true, content: appendRow });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFab) {
|
||||||
|
items.push({ empty: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _memFilterData = memoizeOne(
|
private _memFilterData = memoizeOne(
|
||||||
(
|
(
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
@@ -802,8 +828,8 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
private _checkedRowsChanged() {
|
private _checkedRowsChanged() {
|
||||||
// force scroller to update, change it's items
|
// force scroller to update, change it's items
|
||||||
if (this._items.length) {
|
if (this._filteredData.length) {
|
||||||
this._items = [...this._items];
|
this._filteredData = [...this._filteredData];
|
||||||
}
|
}
|
||||||
fireEvent(this, "selection-changed", {
|
fireEvent(this, "selection-changed", {
|
||||||
value: this._checkedRows,
|
value: this._checkedRows,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { mdiTextureBox } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
@@ -20,12 +20,7 @@ import {
|
|||||||
getDeviceEntityDisplayLookup,
|
getDeviceEntityDisplayLookup,
|
||||||
} from "../data/device_registry";
|
} from "../data/device_registry";
|
||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import {
|
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
|
||||||
FloorRegistryEntry,
|
|
||||||
getFloorAreaLookup,
|
|
||||||
subscribeFloorRegistry,
|
|
||||||
} from "../data/floor_registry";
|
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
@@ -50,7 +45,7 @@ interface FloorAreaEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@customElement("ha-area-floor-picker")
|
@customElement("ha-area-floor-picker")
|
||||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaFloorPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
@@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
|
||||||
|
|
||||||
@state() private _opened?: boolean;
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
private _init = false;
|
private _init = false;
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
|
||||||
return [
|
|
||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
|
||||||
this._floors = floors;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this.comboBox?.open();
|
await this.comboBox?.open();
|
||||||
@@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (
|
if (
|
||||||
(!this._init && this.hass && this._floors) ||
|
(!this._init && this.hass) ||
|
||||||
(this._init && changedProps.has("_opened") && this._opened)
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
) {
|
) {
|
||||||
this._init = true;
|
this._init = true;
|
||||||
const areas = this._getAreas(
|
const areas = this._getAreas(
|
||||||
this._floors!,
|
Object.values(this.hass.floors),
|
||||||
Object.values(this.hass.areas),
|
Object.values(this.hass.areas),
|
||||||
Object.values(this.hass.devices),
|
Object.values(this.hass.devices),
|
||||||
Object.values(this.hass.entities),
|
Object.values(this.hass.entities),
|
||||||
|
155
src/components/ha-badge.ts
Normal file
155
src/components/ha-badge.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import "./ha-ripple";
|
||||||
|
|
||||||
|
type BadgeType = "badge" | "button";
|
||||||
|
|
||||||
|
@customElement("ha-badge")
|
||||||
|
export class HaBadge extends LitElement {
|
||||||
|
@property() public type: BadgeType = "badge";
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "icon-only" }) iconOnly = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const label = this.label;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="badge ${classMap({
|
||||||
|
"icon-only": this.iconOnly,
|
||||||
|
})}"
|
||||||
|
role=${ifDefined(this.type === "button" ? "button" : undefined)}
|
||||||
|
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
|
||||||
|
>
|
||||||
|
<ha-ripple .disabled=${this.type !== "button"}></ha-ripple>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
${this.iconOnly
|
||||||
|
? nothing
|
||||||
|
: html`<span class="info">
|
||||||
|
${label ? html`<span class="label">${label}</span>` : nothing}
|
||||||
|
<span class="content"><slot></slot></span>
|
||||||
|
</span>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
--badge-color: var(--secondary-text-color);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
position: relative;
|
||||||
|
--ha-ripple-color: var(--badge-color);
|
||||||
|
--ha-ripple-hover-opacity: 0.04;
|
||||||
|
--ha-ripple-pressed-opacity: 0.12;
|
||||||
|
transition:
|
||||||
|
box-shadow 180ms ease-in-out,
|
||||||
|
border-color 180ms ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: var(--ha-badge-size, 36px);
|
||||||
|
min-width: var(--ha-badge-size, 36px);
|
||||||
|
padding: 0px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: auto;
|
||||||
|
border-radius: var(
|
||||||
|
--ha-badge-border-radius,
|
||||||
|
calc(var(--ha-badge-size, 36px) / 2)
|
||||||
|
);
|
||||||
|
background: var(
|
||||||
|
--ha-card-background,
|
||||||
|
var(--card-background-color, white)
|
||||||
|
);
|
||||||
|
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||||
|
backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||||
|
border-width: var(--ha-card-border-width, 1px);
|
||||||
|
box-shadow: var(--ha-card-box-shadow, none);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(
|
||||||
|
--ha-card-border-color,
|
||||||
|
var(--divider-color, #e0e0e0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.badge:focus-visible {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--badge-color);
|
||||||
|
border-color: var(--badge-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
[role="button"]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-inline-start: initial;
|
||||||
|
text-align: center;
|
||||||
|
font-family: Roboto;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 10px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
::slotted([slot="icon"]) {
|
||||||
|
--mdc-icon-size: 18px;
|
||||||
|
color: var(--badge-color);
|
||||||
|
line-height: 0;
|
||||||
|
margin-left: -4px;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-inline-start: -4px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
::slotted(img[slot="icon"]) {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: -10px;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-inline-start: -10px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
.badge.icon-only {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.badge.icon-only ::slotted([slot="icon"]) {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-inline-start: 0;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-badge": HaBadge;
|
||||||
|
}
|
||||||
|
}
|
@@ -45,15 +45,35 @@ export class HaConversationAgentPicker extends LitElement {
|
|||||||
if (!this._agents) {
|
if (!this._agents) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const value =
|
let value = this.value;
|
||||||
this.value ??
|
if (!value && this.required) {
|
||||||
(this.required &&
|
// Select Home Assistant conversation agent if it supports the language
|
||||||
(!this.language ||
|
for (const agent of this._agents) {
|
||||||
this._agents
|
if (
|
||||||
.find((agent) => agent.id === "homeassistant")
|
agent.id === "conversation.home_assistant" &&
|
||||||
?.supported_languages.includes(this.language))
|
agent.supported_languages.includes(this.language!)
|
||||||
? "homeassistant"
|
) {
|
||||||
: NONE);
|
value = agent.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
// Select the first agent that supports the language
|
||||||
|
for (const agent of this._agents) {
|
||||||
|
if (
|
||||||
|
agent.supported_languages === "*" &&
|
||||||
|
agent.supported_languages.includes(this.language!)
|
||||||
|
) {
|
||||||
|
value = agent.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
value = NONE;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.label ||
|
.label=${this.label ||
|
||||||
|
@@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
<slot name="icons"></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot name="icons"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="container ${classMap({ expanded: this.expanded })}"
|
class="container ${classMap({ expanded: this.expanded })}"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
LitElement,
|
LitElement,
|
||||||
@@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
import { computeRTL } from "../common/util/compute_rtl";
|
||||||
import {
|
import { getFloorAreaLookup } from "../data/floor_registry";
|
||||||
FloorRegistryEntry,
|
|
||||||
getFloorAreaLookup,
|
|
||||||
subscribeFloorRegistry,
|
|
||||||
} from "../data/floor_registry";
|
|
||||||
import { RelatedResult, findRelated } from "../data/search";
|
import { RelatedResult, findRelated } from "../data/search";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-check-list-item";
|
import "./ha-check-list-item";
|
||||||
@@ -31,7 +25,7 @@ import "./ha-svg-icon";
|
|||||||
import "./ha-tree-indicator";
|
import "./ha-tree-indicator";
|
||||||
|
|
||||||
@customElement("ha-filter-floor-areas")
|
@customElement("ha-filter-floor-areas")
|
||||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
export class HaFilterFloorAreas extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: {
|
@property({ attribute: false }) public value?: {
|
||||||
@@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
@@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const areas = this._areas(this.hass.areas, this._floors);
|
const areas = this._areas(this.hass.areas, this.hass.floors);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
@@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
this._findRelated();
|
this._findRelated();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
|
||||||
return [
|
|
||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
|
||||||
this._floors = floors;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changed) {
|
protected updated(changed) {
|
||||||
if (changed.has("expanded") && this.expanded) {
|
if (changed.has("expanded") && this.expanded) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _areas = memoizeOne(
|
private _areas = memoizeOne(
|
||||||
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
|
||||||
const areas = Object.values(areaReg);
|
const areas = Object.values(areaReg);
|
||||||
|
const floors = Object.values(floorReg);
|
||||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||||
|
|
||||||
const unassisgnedAreas = areas.filter(
|
const unassisgnedAreas = areas.filter(
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
@@ -24,10 +24,8 @@ import {
|
|||||||
FloorRegistryEntry,
|
FloorRegistryEntry,
|
||||||
createFloorRegistryEntry,
|
createFloorRegistryEntry,
|
||||||
getFloorAreaLookup,
|
getFloorAreaLookup,
|
||||||
subscribeFloorRegistry,
|
|
||||||
} from "../data/floor_registry";
|
} from "../data/floor_registry";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
|
||||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||||
@@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
|||||||
</ha-list-item>`;
|
</ha-list-item>`;
|
||||||
|
|
||||||
@customElement("ha-floor-picker")
|
@customElement("ha-floor-picker")
|
||||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
export class HaFloorPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
@@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _opened?: boolean;
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
private _suggestion?: string;
|
private _suggestion?: string;
|
||||||
@@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
await this.comboBox?.focus();
|
await this.comboBox?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
|
||||||
return [
|
|
||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
|
||||||
this._floors = floors;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getFloors = memoizeOne(
|
private _getFloors = memoizeOne(
|
||||||
(
|
(
|
||||||
floors: FloorRegistryEntry[],
|
floors: FloorRegistryEntry[],
|
||||||
@@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
protected updated(changedProps: PropertyValues) {
|
||||||
if (
|
if (
|
||||||
(!this._init && this.hass && this._floors) ||
|
(!this._init && this.hass) ||
|
||||||
(this._init && changedProps.has("_opened") && this._opened)
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
) {
|
) {
|
||||||
this._init = true;
|
this._init = true;
|
||||||
const floors = this._getFloors(
|
const floors = this._getFloors(
|
||||||
this._floors!,
|
Object.values(this.hass.floors),
|
||||||
Object.values(this.hass.areas),
|
Object.values(this.hass.areas),
|
||||||
Object.values(this.hass.devices),
|
Object.values(this.hass.devices),
|
||||||
Object.values(this.hass.entities),
|
Object.values(this.hass.entities),
|
||||||
@@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
? this.hass.localize("ui.components.floor-picker.floor")
|
? this.hass.localize("ui.components.floor-picker.floor")
|
||||||
: this.label}
|
: this.label}
|
||||||
.placeholder=${this.placeholder
|
.placeholder=${this.placeholder
|
||||||
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
|
? this.hass.floors[this.placeholder]?.name
|
||||||
?.name
|
|
||||||
: undefined}
|
: undefined}
|
||||||
.renderer=${rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
floor_id: floor.floor_id,
|
floor_id: floor.floor_id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const floors = [...this._floors!, floor];
|
const floors = [...Object.values(this.hass.floors), floor];
|
||||||
this.comboBox.filteredItems = this._getFloors(
|
this.comboBox.filteredItems = this._getFloors(
|
||||||
floors,
|
floors,
|
||||||
Object.values(this.hass.areas)!,
|
Object.values(this.hass.areas)!,
|
||||||
|
@@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
|
|||||||
} else if (
|
} else if (
|
||||||
"action" in selector ||
|
"action" in selector ||
|
||||||
"trigger" in selector ||
|
"trigger" in selector ||
|
||||||
"condition" in selector ||
|
"condition" in selector
|
||||||
"media" in selector ||
|
|
||||||
"target" in selector
|
|
||||||
) {
|
) {
|
||||||
|
data[field.name] = [];
|
||||||
|
} else if ("media" in selector || "target" in selector) {
|
||||||
data[field.name] = {};
|
data[field.name] = {};
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@@ -21,13 +21,45 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public computeLabel?: (
|
@property({ attribute: false }) public computeLabel?: (
|
||||||
schema: HaFormSchema,
|
schema: HaFormSchema,
|
||||||
data?: HaFormDataContainer
|
data?: HaFormDataContainer,
|
||||||
|
options?: { path?: string[] }
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
@property({ attribute: false }) public computeHelper?: (
|
@property({ attribute: false }) public computeHelper?: (
|
||||||
schema: HaFormSchema
|
schema: HaFormSchema,
|
||||||
|
options?: { path?: string[] }
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
|
private _renderDescription() {
|
||||||
|
const description = this.computeHelper?.(this.schema);
|
||||||
|
return description ? html`<p>${description}</p>` : nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabel = (
|
||||||
|
schema: HaFormSchema,
|
||||||
|
data?: HaFormDataContainer,
|
||||||
|
options?: { path?: string[] }
|
||||||
|
) => {
|
||||||
|
if (!this.computeLabel) return this.computeLabel;
|
||||||
|
|
||||||
|
return this.computeLabel(schema, data, {
|
||||||
|
...options,
|
||||||
|
path: [...(options?.path || []), this.schema.name],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private _computeHelper = (
|
||||||
|
schema: HaFormSchema,
|
||||||
|
options?: { path?: string[] }
|
||||||
|
) => {
|
||||||
|
if (!this.computeHelper) return this.computeHelper;
|
||||||
|
|
||||||
|
return this.computeHelper(schema, {
|
||||||
|
...options,
|
||||||
|
path: [...(options?.path || []), this.schema.name],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
|
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
|
||||||
@@ -43,16 +75,17 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
|||||||
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
|
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this.schema.title}
|
${this.schema.title || this.computeLabel?.(this.schema)}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
${this._renderDescription()}
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.data=${this.data}
|
.data=${this.data}
|
||||||
.schema=${this.schema.schema}
|
.schema=${this.schema.schema}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.computeLabel=${this.computeLabel}
|
.computeLabel=${this._computeLabel}
|
||||||
.computeHelper=${this.computeHelper}
|
.computeHelper=${this._computeHelper}
|
||||||
></ha-form>
|
></ha-form>
|
||||||
</div>
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
@@ -71,6 +104,9 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
|||||||
.content {
|
.content {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
.content p {
|
||||||
|
margin: 0 0 24px;
|
||||||
|
}
|
||||||
ha-expansion-panel {
|
ha-expansion-panel {
|
||||||
display: block;
|
display: block;
|
||||||
--expansion-panel-content-padding: 0;
|
--expansion-panel-content-padding: 0;
|
||||||
|
@@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getValue = (obj, item) =>
|
const getValue = (obj, item) =>
|
||||||
obj ? (!item.name ? obj : obj[item.name]) : null;
|
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
|
||||||
|
|
||||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||||
|
|
||||||
@@ -204,9 +204,10 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
|
|
||||||
if (ev.target === this) return;
|
if (ev.target === this) return;
|
||||||
|
|
||||||
const newValue = !schema.name
|
const newValue =
|
||||||
? ev.detail.value
|
!schema.name || ("flatten" in schema && schema.flatten)
|
||||||
: { [schema.name]: ev.detail.value };
|
? ev.detail.value
|
||||||
|
: { [schema.name]: ev.detail.value };
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
...this.data,
|
...this.data,
|
||||||
|
@@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
|
|||||||
|
|
||||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||||
type: "grid";
|
type: "grid";
|
||||||
name: string;
|
flatten?: boolean;
|
||||||
column_min_width?: string;
|
column_min_width?: string;
|
||||||
schema: readonly HaFormSchema[];
|
schema: readonly HaFormSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HaFormExpandableSchema extends HaFormBaseSchema {
|
export interface HaFormExpandableSchema extends HaFormBaseSchema {
|
||||||
type: "expandable";
|
type: "expandable";
|
||||||
name: "";
|
flatten?: boolean;
|
||||||
title: string;
|
title?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconPath?: string;
|
iconPath?: string;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
@@ -100,7 +100,7 @@ export type SchemaUnion<
|
|||||||
SchemaArray extends readonly HaFormSchema[],
|
SchemaArray extends readonly HaFormSchema[],
|
||||||
Schema = SchemaArray[number],
|
Schema = SchemaArray[number],
|
||||||
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
|
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
|
||||||
? SchemaUnion<Schema["schema"]>
|
? SchemaUnion<Schema["schema"]> | Schema
|
||||||
: Schema;
|
: Schema;
|
||||||
|
|
||||||
export interface HaFormDataContainer {
|
export interface HaFormDataContainer {
|
||||||
|
@@ -18,9 +18,9 @@ export class HaFormfield extends FormfieldBase {
|
|||||||
|
|
||||||
return html` <div class="mdc-form-field ${classMap(classes)}">
|
return html` <div class="mdc-form-field ${classMap(classes)}">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<label class="mdc-label" @click=${this._labelClick}
|
<label class="mdc-label" @click=${this._labelClick}>
|
||||||
><slot name="label">${this.label}</slot></label
|
<slot name="label">${this.label}</slot>
|
||||||
>
|
</label>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +57,13 @@ export class HaFormfield extends FormfieldBase {
|
|||||||
}
|
}
|
||||||
.mdc-form-field {
|
.mdc-form-field {
|
||||||
align-items: var(--ha-formfield-align-items, center);
|
align-items: var(--ha-formfield-align-items, center);
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.mdc-form-field > label {
|
.mdc-form-field > label {
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
margin-inline-start: 0;
|
margin-inline-start: 0;
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
padding-inline-start: 4px;
|
padding: 0;
|
||||||
padding-inline-end: 0;
|
|
||||||
}
|
}
|
||||||
:host([disabled]) label {
|
:host([disabled]) label {
|
||||||
color: var(--disabled-text-color);
|
color: var(--disabled-text-color);
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "./ha-icon-button";
|
|
||||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
|
||||||
import { mdiRestore } from "@mdi/js";
|
import { mdiRestore } from "@mdi/js";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../types";
|
|
||||||
import { conditionalClamp } from "../common/number/clamp";
|
import { conditionalClamp } from "../common/number/clamp";
|
||||||
|
import {
|
||||||
type GridSizeValue = {
|
CardGridSize,
|
||||||
rows?: number | "auto";
|
DEFAULT_GRID_SIZE,
|
||||||
columns?: number;
|
} from "../panels/lovelace/common/compute-card-grid-size";
|
||||||
};
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-grid-size-picker")
|
@customElement("ha-grid-size-picker")
|
||||||
export class HaGridSizeEditor extends LitElement {
|
export class HaGridSizeEditor extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: GridSizeValue;
|
@property({ attribute: false }) public value?: CardGridSize;
|
||||||
|
|
||||||
@property({ attribute: false }) public rows = 8;
|
@property({ attribute: false }) public rows = 8;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public isDefault?: boolean;
|
@property({ attribute: false }) public isDefault?: boolean;
|
||||||
|
|
||||||
@state() public _localValue?: GridSizeValue = undefined;
|
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
|
||||||
|
|
||||||
protected willUpdate(changedProperties) {
|
protected willUpdate(changedProperties) {
|
||||||
if (changedProperties.has("value")) {
|
if (changedProperties.has("value")) {
|
||||||
@@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
this.rowMin !== undefined && this.rowMin === this.rowMax;
|
this.rowMin !== undefined && this.rowMin === this.rowMax;
|
||||||
|
|
||||||
const autoHeight = this._localValue?.rows === "auto";
|
const autoHeight = this._localValue?.rows === "auto";
|
||||||
|
const fullWidth = this._localValue?.columns === "full";
|
||||||
|
|
||||||
const rowMin = this.rowMin ?? 1;
|
const rowMin = this.rowMin ?? 1;
|
||||||
const rowMax = this.rowMax ?? this.rows;
|
const rowMax = this.rowMax ?? this.rows;
|
||||||
@@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
.min=${columnMin}
|
.min=${columnMin}
|
||||||
.max=${columnMax}
|
.max=${columnMax}
|
||||||
.range=${this.columns}
|
.range=${this.columns}
|
||||||
.value=${columnValue}
|
.value=${fullWidth ? this.columns : columnValue}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
@slider-moved=${this._sliderMoved}
|
@slider-moved=${this._sliderMoved}
|
||||||
.disabled=${disabledColumns}
|
.disabled=${disabledColumns}
|
||||||
@@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<div
|
<div
|
||||||
class="preview"
|
class="preview ${classMap({ "full-width": fullWidth })}"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
"--total-rows": this.rows,
|
"--total-rows": this.rows,
|
||||||
"--total-columns": this.columns,
|
"--total-columns": this.columns,
|
||||||
"--rows": rowValue,
|
"--rows": rowValue,
|
||||||
"--columns": columnValue,
|
"--columns": fullWidth ? this.columns : columnValue,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
const cell = ev.currentTarget as HTMLElement;
|
const cell = ev.currentTarget as HTMLElement;
|
||||||
const rows = Number(cell.getAttribute("data-row"));
|
const rows = Number(cell.getAttribute("data-row"));
|
||||||
const columns = Number(cell.getAttribute("data-column"));
|
const columns = Number(cell.getAttribute("data-column"));
|
||||||
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
|
const clampedRow: CardGridSize["rows"] = conditionalClamp(
|
||||||
const clampedColumn = conditionalClamp(
|
rows,
|
||||||
|
this.rowMin,
|
||||||
|
this.rowMax
|
||||||
|
);
|
||||||
|
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
|
||||||
columns,
|
columns,
|
||||||
this.columnMin,
|
this.columnMin,
|
||||||
this.columnMax
|
this.columnMax
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||||
|
if (currentSize.columns === "full" && clampedColumn === this.columns) {
|
||||||
|
clampedColumn = "full";
|
||||||
|
}
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { rows: clampedRow, columns: clampedColumn },
|
value: { rows: clampedRow, columns: clampedColumn },
|
||||||
});
|
});
|
||||||
@@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
|
|
||||||
private _valueChanged(ev) {
|
private _valueChanged(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const key = ev.currentTarget.id;
|
const key = ev.currentTarget.id as "rows" | "columns";
|
||||||
const newValue = {
|
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||||
...this.value,
|
let value = ev.detail.value as CardGridSize[typeof key];
|
||||||
[key]: ev.detail.value,
|
|
||||||
|
if (
|
||||||
|
key === "columns" &&
|
||||||
|
currentSize.columns === "full" &&
|
||||||
|
value === this.columns
|
||||||
|
) {
|
||||||
|
value = "full";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSize = {
|
||||||
|
...currentSize,
|
||||||
|
[key]: value,
|
||||||
};
|
};
|
||||||
fireEvent(this, "value-changed", { value: newValue });
|
fireEvent(this, "value-changed", { value: newSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _reset(ev) {
|
private _reset(ev) {
|
||||||
@@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
|
|
||||||
private _sliderMoved(ev) {
|
private _sliderMoved(ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const key = ev.currentTarget.id;
|
const key = ev.currentTarget.id as "rows" | "columns";
|
||||||
const value = ev.detail.value;
|
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||||
|
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
|
||||||
|
|
||||||
if (value === undefined) return;
|
if (value === undefined) return;
|
||||||
|
|
||||||
this._localValue = {
|
this._localValue = {
|
||||||
...this.value,
|
...currentSize,
|
||||||
[key]: ev.detail.value,
|
[key]: ev.detail.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -189,7 +213,7 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"reset column-slider"
|
"reset column-slider"
|
||||||
"row-slider preview";
|
"row-slider preview";
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto auto;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@@ -205,17 +229,12 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
.preview {
|
.preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-area: preview;
|
grid-area: preview;
|
||||||
aspect-ratio: 1 / 1.2;
|
|
||||||
}
|
}
|
||||||
.preview > div {
|
.preview > div {
|
||||||
position: absolute;
|
position: relative;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--total-columns), 1fr);
|
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||||
grid-template-rows: repeat(var(--total-rows), 1fr);
|
grid-template-rows: repeat(var(--total-rows), 25px);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.preview .cell {
|
.preview .cell {
|
||||||
@@ -226,15 +245,23 @@ export class HaGridSizeEditor extends LitElement {
|
|||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.selected {
|
.preview .selected {
|
||||||
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.selected .cell {
|
.selected .cell {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
grid-column: 1 / span var(--columns, 0);
|
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
|
||||||
grid-row: 1 / span var(--rows, 0);
|
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
.preview.full-width .selected .cell {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -96,7 +96,25 @@ class HaMarkdownElement extends ReactiveElement {
|
|||||||
|
|
||||||
haAlertNode.append(
|
haAlertNode.append(
|
||||||
...Array.from(node.childNodes)
|
...Array.from(node.childNodes)
|
||||||
.map((child) => Array.from(child.childNodes))
|
.map((child) => {
|
||||||
|
const arr = Array.from(child.childNodes);
|
||||||
|
if (!this.breaks && arr.length) {
|
||||||
|
// When we are not breaking, the first line of the blockquote is not considered,
|
||||||
|
// so we need to adjust the first child text content
|
||||||
|
const firstChild = arr[0];
|
||||||
|
if (
|
||||||
|
firstChild.nodeType === Node.TEXT_NODE &&
|
||||||
|
firstChild.textContent === gitHubAlertMatch.input &&
|
||||||
|
firstChild.textContent?.includes("\n")
|
||||||
|
) {
|
||||||
|
firstChild.textContent = firstChild.textContent
|
||||||
|
.split("\n")
|
||||||
|
.slice(1)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
})
|
||||||
.reduce((acc, val) => acc.concat(val), [])
|
.reduce((acc, val) => acc.concat(val), [])
|
||||||
.filter(
|
.filter(
|
||||||
(childNode) =>
|
(childNode) =>
|
||||||
|
@@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
|
|||||||
import "./ha-menu";
|
import "./ha-menu";
|
||||||
import type { HaMenu } from "./ha-menu";
|
import type { HaMenu } from "./ha-menu";
|
||||||
|
|
||||||
@customElement("ha-button-menu-new")
|
@customElement("ha-md-button-menu")
|
||||||
export class HaButtonMenuNew extends LitElement {
|
export class HaMdButtonMenu extends LitElement {
|
||||||
protected readonly [FOCUS_TARGET];
|
protected readonly [FOCUS_TARGET];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
@@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-button-menu-new": HaButtonMenuNew;
|
"ha-md-button-menu": HaMdButtonMenu;
|
||||||
}
|
}
|
||||||
}
|
}
|
210
src/components/ha-md-dialog.ts
Normal file
210
src/components/ha-md-dialog.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { MdDialog } from "@material/web/dialog/dialog";
|
||||||
|
import {
|
||||||
|
type DialogAnimation,
|
||||||
|
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||||
|
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||||
|
} from "@material/web/dialog/internal/animations";
|
||||||
|
import { css } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
|
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@customElement("ha-md-dialog")
|
||||||
|
export class HaMdDialog extends MdDialog {
|
||||||
|
/**
|
||||||
|
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||||
|
*/
|
||||||
|
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||||
|
public disableCancelAction = false;
|
||||||
|
|
||||||
|
private _polyfillDialogRegistered = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.addEventListener("cancel", this._handleCancel);
|
||||||
|
|
||||||
|
if (typeof HTMLDialogElement !== "function") {
|
||||||
|
this.addEventListener("open", this._handleOpen);
|
||||||
|
|
||||||
|
if (!DIALOG_POLYFILL) {
|
||||||
|
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if browser doesn't support animate API disable open/close animations
|
||||||
|
if (this.animate === undefined) {
|
||||||
|
this.quick = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent open in older browsers and wait for polyfill to load
|
||||||
|
private async _handleOpen(openEvent: Event) {
|
||||||
|
openEvent.preventDefault();
|
||||||
|
|
||||||
|
if (this._polyfillDialogRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._polyfillDialogRegistered = true;
|
||||||
|
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||||
|
const dialog = this.shadowRoot?.querySelector(
|
||||||
|
"dialog"
|
||||||
|
) as HTMLDialogElement;
|
||||||
|
|
||||||
|
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||||
|
dialogPolyfill.default.registerDialog(dialog);
|
||||||
|
this.removeEventListener("open", this._handleOpen);
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadPolyfillStylesheet(href) {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href;
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
link.onload = () => resolve();
|
||||||
|
link.onerror = () =>
|
||||||
|
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||||
|
|
||||||
|
this.shadowRoot?.appendChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleCancel(closeEvent: Event) {
|
||||||
|
if (this.disableCancelAction) {
|
||||||
|
closeEvent.preventDefault();
|
||||||
|
const dialogElement = this.shadowRoot?.querySelector("dialog");
|
||||||
|
if (this.animate !== undefined) {
|
||||||
|
dialogElement?.animate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
transform: "rotate(-1deg)",
|
||||||
|
"animation-timing-function": "ease-in",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: "rotate(1.5deg)",
|
||||||
|
"animation-timing-function": "ease-out",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: "rotate(0deg)",
|
||||||
|
"animation-timing-function": "ease-in",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
duration: 200,
|
||||||
|
iterations: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static override styles = [
|
||||||
|
...super.styles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
--md-dialog-container-color: var(--card-background-color);
|
||||||
|
--md-dialog-headline-color: var(--primary-text-color);
|
||||||
|
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||||
|
--md-sys-color-scrim: #000000;
|
||||||
|
|
||||||
|
--md-dialog-headline-weight: 400;
|
||||||
|
--md-dialog-headline-size: 1.574rem;
|
||||||
|
--md-dialog-supporting-text-size: 1rem;
|
||||||
|
--md-dialog-supporting-text-line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([type="alert"]) {
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:not([type="alert"])) {
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
min-width: calc(
|
||||||
|
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||||
|
);
|
||||||
|
max-width: calc(
|
||||||
|
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||||
|
);
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
--md-dialog-container-shape: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host ::slotted(ha-dialog-header) {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrim {
|
||||||
|
z-index: 10; // overlay navigation
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// by default the dialog open/close animation will be from/to the top
|
||||||
|
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
|
||||||
|
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||||
|
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||||
|
dialog: [
|
||||||
|
[
|
||||||
|
// Dialog slide up
|
||||||
|
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||||
|
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
container: [
|
||||||
|
[
|
||||||
|
// Container fade in
|
||||||
|
[{ opacity: 0 }, { opacity: 1 }],
|
||||||
|
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||||
|
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||||
|
dialog: [
|
||||||
|
[
|
||||||
|
// Dialog slide down
|
||||||
|
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||||
|
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
container: [
|
||||||
|
[
|
||||||
|
// Container fade out
|
||||||
|
[{ opacity: "1" }, { opacity: "0" }],
|
||||||
|
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMobileOpenFromBottomAnimation = () => {
|
||||||
|
const matches = window.matchMedia(
|
||||||
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
|
).matches;
|
||||||
|
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMobileCloseToBottomAnimation = () => {
|
||||||
|
const matches = window.matchMedia(
|
||||||
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
|
).matches;
|
||||||
|
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-md-dialog": HaMdDialog;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-list-item-new")
|
@customElement("ha-md-list-item")
|
||||||
export class HaListItemNew extends MdListItem {
|
export class HaMdListItem extends MdListItem {
|
||||||
static override styles = [
|
static override styles = [
|
||||||
...super.styles,
|
...super.styles,
|
||||||
css`
|
css`
|
||||||
@@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-list-item-new": HaListItemNew;
|
"ha-md-list-item": HaMdListItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-list-new")
|
@customElement("ha-md-list")
|
||||||
export class HaListNew extends MdList {
|
export class HaMdList extends MdList {
|
||||||
static override styles = [
|
static override styles = [
|
||||||
...super.styles,
|
...super.styles,
|
||||||
css`
|
css`
|
||||||
@@ -16,6 +16,6 @@ export class HaListNew extends MdList {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-list-new": HaListNew;
|
"ha-md-list": HaMdList;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
|
|||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-menu-item")
|
@customElement("ha-md-menu-item")
|
||||||
export class HaMenuItem extends MdMenuItem {
|
export class HaMdMenuItem extends MdMenuItem {
|
||||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
@@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-menu-item": HaMenuItem;
|
"ha-md-menu-item": HaMdMenuItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@material/web/menu/internal/controllers/shared";
|
} from "@material/web/menu/internal/controllers/shared";
|
||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { HaMenuItem } from "./ha-menu-item";
|
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||||
|
|
||||||
@customElement("ha-menu")
|
@customElement("ha-menu")
|
||||||
export class HaMenu extends MdMenu {
|
export class HaMenu extends MdMenu {
|
||||||
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
|
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||||
}
|
}
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
|
@@ -1,19 +1,15 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { BooleanSelector } from "../../data/selector";
|
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import "../ha-checkbox";
|
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
import "../ha-input-helper-text";
|
|
||||||
import "../ha-switch";
|
import "../ha-switch";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
|
|
||||||
@customElement("ha-selector-boolean")
|
@customElement("ha-selector-boolean")
|
||||||
export class HaBooleanSelector extends LitElement {
|
export class HaBooleanSelector extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: BooleanSelector;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public value = false;
|
@property({ type: Boolean }) public value = false;
|
||||||
|
|
||||||
@property() public placeholder?: any;
|
@property() public placeholder?: any;
|
||||||
@@ -25,28 +21,20 @@ export class HaBooleanSelector extends LitElement {
|
|||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const checkbox = this.selector.boolean?.mode === "checkbox";
|
|
||||||
return html`
|
return html`
|
||||||
<ha-formfield .alignEnd=${!checkbox} spaceBetween .label=${this.label}>
|
<ha-formfield alignEnd spaceBetween .label=${this.label}>
|
||||||
${checkbox
|
<ha-switch
|
||||||
? html`
|
.checked=${this.value ?? this.placeholder === true}
|
||||||
<ha-checkbox
|
@change=${this._handleChange}
|
||||||
.checked=${this.value ?? this.placeholder === true}
|
.disabled=${this.disabled}
|
||||||
@change=${this._handleChange}
|
></ha-switch>
|
||||||
.disabled=${this.disabled}
|
<span slot="label">
|
||||||
></ha-checkbox>
|
<p class="primary">${this.label}</p>
|
||||||
`
|
${this.helper
|
||||||
: html`
|
? html`<p class="secondary">${this.helper}</p>`
|
||||||
<ha-switch
|
: nothing}
|
||||||
.checked=${this.value ?? this.placeholder === true}
|
</span>
|
||||||
@change=${this._handleChange}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
></ha-switch>
|
|
||||||
`}
|
|
||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
${this.helper
|
|
||||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
|
||||||
: ""}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +50,21 @@ export class HaBooleanSelector extends LitElement {
|
|||||||
return css`
|
return css`
|
||||||
ha-formfield {
|
ha-formfield {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 56px;
|
min-height: 56px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
--mdc-typography-body2-font-size: 1em;
|
--mdc-typography-body2-font-size: 1em;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.secondary {
|
||||||
|
direction: var(--direction);
|
||||||
|
padding-top: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--mdc-typography-body2-font-weight, 400);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
|
|||||||
.label=${this.label || ""}
|
.label=${this.label || ""}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disalbled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@change=${this._valueChanged}
|
@change=${this._valueChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
`;
|
`;
|
||||||
|
@@ -162,8 +162,14 @@ export class HaLocationSelector extends LitElement {
|
|||||||
|
|
||||||
private _computeLabel = (
|
private _computeLabel = (
|
||||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
): string =>
|
): string => {
|
||||||
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
|
if (entry.name) {
|
||||||
|
return this.hass.localize(
|
||||||
|
`ui.components.selectors.location.${entry.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
ha-locations-editor {
|
ha-locations-editor {
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
@@ -60,12 +67,12 @@ export class HaNumberSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${this.label && !isBox
|
||||||
|
? html`${this.label}${this.required ? "*" : ""}`
|
||||||
|
: nothing}
|
||||||
<div class="input">
|
<div class="input">
|
||||||
${!isBox
|
${!isBox
|
||||||
? html`
|
? html`
|
||||||
${this.label
|
|
||||||
? html`${this.label}${this.required ? "*" : ""}`
|
|
||||||
: ""}
|
|
||||||
<ha-slider
|
<ha-slider
|
||||||
labeled
|
labeled
|
||||||
.min=${this.selector.number!.min}
|
.min=${this.selector.number!.min}
|
||||||
@@ -75,10 +82,11 @@ export class HaNumberSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
@change=${this._handleSliderChange}
|
@change=${this._handleSliderChange}
|
||||||
|
.ticks=${this.selector.number?.slider_ticks}
|
||||||
>
|
>
|
||||||
</ha-slider>
|
</ha-slider>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
.inputMode=${this.selector.number?.step === "any" ||
|
.inputMode=${this.selector.number?.step === "any" ||
|
||||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||||
@@ -105,7 +113,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${!isBox && this.helper
|
${!isBox && this.helper
|
||||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||||
: ""}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +149,9 @@ export class HaNumberSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
ha-slider {
|
ha-slider {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
margin-right: 16px;
|
||||||
|
margin-inline-end: 16px;
|
||||||
|
margin-inline-start: 0;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
--ha-textfield-input-width: 40px;
|
--ha-textfield-input-width: 40px;
|
||||||
|
@@ -82,6 +82,7 @@ export class HaTextSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
|
.type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
|
||||||
@input=${this._handleChange}
|
@input=${this._handleChange}
|
||||||
|
@change=${this._handleChange}
|
||||||
.label=${this.label || ""}
|
.label=${this.label || ""}
|
||||||
.prefix=${this.selector.text?.prefix}
|
.prefix=${this.selector.text?.prefix}
|
||||||
.suffix=${this.selector.text?.type === "password"
|
.suffix=${this.selector.text?.type === "password"
|
||||||
|
@@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
|
|||||||
clearable
|
clearable
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
enable-second
|
.enableSecond=${!this.selector.time?.no_second}
|
||||||
></ha-time-input>
|
></ha-time-input>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -44,6 +44,7 @@ import "./ha-service-picker";
|
|||||||
import "./ha-settings-row";
|
import "./ha-settings-row";
|
||||||
import "./ha-yaml-editor";
|
import "./ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||||
|
import "./ha-service-section-icon";
|
||||||
|
|
||||||
const attributeFilter = (values: any[], attribute: any) => {
|
const attributeFilter = (values: any[], attribute: any) => {
|
||||||
if (typeof attribute === "object") {
|
if (typeof attribute === "object") {
|
||||||
@@ -239,12 +240,24 @@ export class HaServiceControl extends LitElement {
|
|||||||
...value,
|
...value,
|
||||||
selector: value.selector as Selector | undefined,
|
selector: value.selector as Selector | undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const hasSelector: string[] = [];
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if ((field as any).fields) {
|
||||||
|
Object.entries((field as any).fields).forEach(([key, subField]) => {
|
||||||
|
if ((subField as any).selector) {
|
||||||
|
hasSelector.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (field.selector) {
|
||||||
|
hasSelector.push(field.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...serviceDomains[domain][serviceName],
|
...serviceDomains[domain][serviceName],
|
||||||
fields,
|
fields,
|
||||||
hasSelector: fields.length
|
hasSelector,
|
||||||
? fields.filter((field) => field.selector).map((field) => field.key)
|
|
||||||
: [],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -496,12 +509,18 @@ export class HaServiceControl extends LitElement {
|
|||||||
) ||
|
) ||
|
||||||
dataField.name ||
|
dataField.name ||
|
||||||
dataField.key}
|
dataField.key}
|
||||||
>
|
.secondary=${this._getSectionDescription(
|
||||||
${this._renderSectionDescription(
|
|
||||||
dataField,
|
dataField,
|
||||||
domain,
|
domain,
|
||||||
serviceName
|
serviceName
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<ha-service-section-icon
|
||||||
|
slot="icons"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.service=${this._value!.action}
|
||||||
|
.section=${dataField.key}
|
||||||
|
></ha-service-section-icon>
|
||||||
${Object.entries(dataField.fields).map(([key, field]) =>
|
${Object.entries(dataField.fields).map(([key, field]) =>
|
||||||
this._renderField(
|
this._renderField(
|
||||||
{ key, ...field },
|
{ key, ...field },
|
||||||
@@ -522,20 +541,14 @@ export class HaServiceControl extends LitElement {
|
|||||||
)} `;
|
)} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderSectionDescription(
|
private _getSectionDescription(
|
||||||
dataField: ExtHassService["fields"][number],
|
dataField: ExtHassService["fields"][number],
|
||||||
domain: string | undefined,
|
domain: string | undefined,
|
||||||
serviceName: string | undefined
|
serviceName: string | undefined
|
||||||
) {
|
) {
|
||||||
const description = this.hass!.localize(
|
return this.hass!.localize(
|
||||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
|
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!description) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`<p>${description}</p>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderField = (
|
private _renderField = (
|
||||||
|
53
src/components/ha-service-section-icon.ts
Normal file
53
src/components/ha-service-section-icon.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { until } from "lit/directives/until";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
import { serviceSectionIcon } from "../data/icons";
|
||||||
|
|
||||||
|
@customElement("ha-service-section-icon")
|
||||||
|
export class HaServiceSectionIcon extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public service?: string;
|
||||||
|
|
||||||
|
@property() public section?: string;
|
||||||
|
|
||||||
|
@property() public icon?: string;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (this.icon) {
|
||||||
|
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.service || !this.section) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hass) {
|
||||||
|
return this._renderFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
|
||||||
|
(icn) => {
|
||||||
|
if (icn) {
|
||||||
|
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||||
|
}
|
||||||
|
return this._renderFallback();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`${until(icon)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderFallback() {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-service-section-icon": HaServiceSectionIcon;
|
||||||
|
}
|
||||||
|
}
|
@@ -20,6 +20,7 @@ export class HaSlider extends MdSlider {
|
|||||||
--md-sys-color-on-surface: var(--primary-text-color);
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
--md-slider-handle-width: 14px;
|
--md-slider-handle-width: 14px;
|
||||||
--md-slider-handle-height: 14px;
|
--md-slider-handle-height: 14px;
|
||||||
|
--md-slider-state-layer-size: 24px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-inline-size: 100px;
|
min-inline-size: 100px;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
@@ -16,11 +16,10 @@ import { HomeAssistant } from "../types";
|
|||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
|
||||||
const NONE = "__NONE_OPTION__";
|
const NONE = "__NONE_OPTION__";
|
||||||
|
|
||||||
const NAME_MAP = { cloud: "Home Assistant Cloud" };
|
|
||||||
|
|
||||||
@customElement("ha-stt-picker")
|
@customElement("ha-stt-picker")
|
||||||
export class HaSTTPicker extends LitElement {
|
export class HaSTTPicker extends LitElement {
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@@ -41,13 +40,32 @@ export class HaSTTPicker extends LitElement {
|
|||||||
if (!this._engines) {
|
if (!this._engines) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const value =
|
|
||||||
this.value ??
|
let value = this.value;
|
||||||
(this.required
|
if (!value && this.required) {
|
||||||
? this._engines.find(
|
for (const entity of Object.values(this.hass.entities)) {
|
||||||
(engine) => engine.supported_languages?.length !== 0
|
if (
|
||||||
)
|
entity.platform === "cloud" &&
|
||||||
: NONE);
|
computeDomain(entity.entity_id) === "stt"
|
||||||
|
) {
|
||||||
|
value = entity.entity_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
for (const sttEngine of this._engines) {
|
||||||
|
if (sttEngine?.supported_languages?.length !== 0) {
|
||||||
|
value = sttEngine.engine_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
value = NONE;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.label ||
|
.label=${this.label ||
|
||||||
@@ -66,12 +84,15 @@ export class HaSTTPicker extends LitElement {
|
|||||||
</ha-list-item>`
|
</ha-list-item>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._engines.map((engine) => {
|
${this._engines.map((engine) => {
|
||||||
let label = engine.engine_id;
|
if (engine.deprecated && engine.engine_id !== value) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
let label: string;
|
||||||
if (engine.engine_id.includes(".")) {
|
if (engine.engine_id.includes(".")) {
|
||||||
const stateObj = this.hass!.states[engine.engine_id];
|
const stateObj = this.hass!.states[engine.engine_id];
|
||||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||||
} else if (engine.engine_id in NAME_MAP) {
|
} else {
|
||||||
label = NAME_MAP[engine.engine_id];
|
label = engine.name || engine.engine_id;
|
||||||
}
|
}
|
||||||
return html`<ha-list-item
|
return html`<ha-list-item
|
||||||
.value=${engine.engine_id}
|
.value=${engine.engine_id}
|
||||||
|
@@ -35,10 +35,6 @@ import {
|
|||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
} from "../data/device_registry";
|
} from "../data/device_registry";
|
||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||||
import {
|
|
||||||
FloorRegistryEntry,
|
|
||||||
subscribeFloorRegistry,
|
|
||||||
} from "../data/floor_registry";
|
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
@@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
|
||||||
|
|
||||||
@state() private _labels?: LabelRegistryEntry[];
|
@state() private _labels?: LabelRegistryEntry[];
|
||||||
|
|
||||||
private _opened = false;
|
private _opened = false;
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
return [
|
return [
|
||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
|
||||||
this._floors = floors;
|
|
||||||
}),
|
|
||||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
this._labels = labels;
|
this._labels = labels;
|
||||||
}),
|
}),
|
||||||
@@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
<div class="mdc-chip-set items">
|
<div class="mdc-chip-set items">
|
||||||
${this.value?.floor_id
|
${this.value?.floor_id
|
||||||
? ensureArray(this.value.floor_id).map((floor_id) => {
|
? ensureArray(this.value.floor_id).map((floor_id) => {
|
||||||
const floor = this._floors?.find(
|
const floor = this.hass.floors[floor_id];
|
||||||
(flr) => flr.floor_id === floor_id
|
|
||||||
);
|
|
||||||
return this._renderChip(
|
return this._renderChip(
|
||||||
"floor_id",
|
"floor_id",
|
||||||
floor_id,
|
floor_id,
|
||||||
|
@@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase {
|
|||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-text-field__icon {
|
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,14 +16,10 @@ import { HomeAssistant } from "../types";
|
|||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
|
||||||
const NONE = "__NONE_OPTION__";
|
const NONE = "__NONE_OPTION__";
|
||||||
|
|
||||||
const NAME_MAP = {
|
|
||||||
cloud: "Home Assistant Cloud",
|
|
||||||
google_translate: "Google Translate",
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement("ha-tts-picker")
|
@customElement("ha-tts-picker")
|
||||||
export class HaTTSPicker extends LitElement {
|
export class HaTTSPicker extends LitElement {
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@@ -44,13 +40,32 @@ export class HaTTSPicker extends LitElement {
|
|||||||
if (!this._engines) {
|
if (!this._engines) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const value =
|
|
||||||
this.value ??
|
let value = this.value;
|
||||||
(this.required
|
if (!value && this.required) {
|
||||||
? this._engines.find(
|
for (const entity of Object.values(this.hass.entities)) {
|
||||||
(engine) => engine.supported_languages?.length !== 0
|
if (
|
||||||
)
|
entity.platform === "cloud" &&
|
||||||
: NONE);
|
computeDomain(entity.entity_id) === "tts"
|
||||||
|
) {
|
||||||
|
value = entity.entity_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
for (const ttsEngine of this._engines) {
|
||||||
|
if (ttsEngine?.supported_languages?.length !== 0) {
|
||||||
|
value = ttsEngine.engine_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
value = NONE;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-select
|
<ha-select
|
||||||
.label=${this.label ||
|
.label=${this.label ||
|
||||||
@@ -69,12 +84,15 @@ export class HaTTSPicker extends LitElement {
|
|||||||
</ha-list-item>`
|
</ha-list-item>`
|
||||||
: nothing}
|
: nothing}
|
||||||
${this._engines.map((engine) => {
|
${this._engines.map((engine) => {
|
||||||
let label = engine.engine_id;
|
if (engine.deprecated && engine.engine_id !== value) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
let label: string;
|
||||||
if (engine.engine_id.includes(".")) {
|
if (engine.engine_id.includes(".")) {
|
||||||
const stateObj = this.hass!.states[engine.engine_id];
|
const stateObj = this.hass!.states[engine.engine_id];
|
||||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||||
} else if (engine.engine_id in NAME_MAP) {
|
} else {
|
||||||
label = NAME_MAP[engine.engine_id];
|
label = engine.name || engine.engine_id;
|
||||||
}
|
}
|
||||||
return html`<ha-list-item
|
return html`<ha-list-item
|
||||||
.value=${engine.engine_id}
|
.value=${engine.engine_id}
|
||||||
|
@@ -109,7 +109,7 @@ class HaWebRtcPlayer extends LitElement {
|
|||||||
let candidates = ""; // Build an Offer SDP string with ice candidates
|
let candidates = ""; // Build an Offer SDP string with ice candidates
|
||||||
const iceResolver = new Promise<void>((resolve) => {
|
const iceResolver = new Promise<void>((resolve) => {
|
||||||
peerConnection.addEventListener("icecandidate", async (event) => {
|
peerConnection.addEventListener("icecandidate", async (event) => {
|
||||||
if (!event.candidate) {
|
if (!event.candidate?.candidate) {
|
||||||
resolve(); // Gathering complete
|
resolve(); // Gathering complete
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { Condition, Trigger } from "../../data/automation";
|
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
|
||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
ChooseAction,
|
ChooseAction,
|
||||||
@@ -572,8 +572,8 @@ export class HatScriptGraph extends LitElement {
|
|||||||
const paths = Object.keys(this.trackedNodes);
|
const paths = Object.keys(this.trackedNodes);
|
||||||
const trigger_nodes =
|
const trigger_nodes =
|
||||||
"trigger" in this.trace.config
|
"trigger" in this.trace.config
|
||||||
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
|
? flattenTriggers(ensureArray(this.trace.config.trigger)).map(
|
||||||
this.render_trigger(trigger, i)
|
(trigger, i) => this.render_trigger(trigger, i)
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
try {
|
try {
|
||||||
|
@@ -3,6 +3,7 @@ import {
|
|||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
|
import { ensureArray } from "../common/array/ensure-array";
|
||||||
import { Context, HomeAssistant } from "../types";
|
import { Context, HomeAssistant } from "../types";
|
||||||
import { BlueprintInput } from "./blueprint";
|
import { BlueprintInput } from "./blueprint";
|
||||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
@@ -62,6 +63,10 @@ export interface ContextConstraint {
|
|||||||
user_id?: string | string[];
|
user_id?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TriggerList {
|
||||||
|
triggers: Trigger | Trigger[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseTrigger {
|
export interface BaseTrigger {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -373,6 +378,27 @@ export const normalizeAutomationConfig = <
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const flattenTriggers = (
|
||||||
|
triggers: undefined | (Trigger | TriggerList)[]
|
||||||
|
): Trigger[] => {
|
||||||
|
if (!triggers) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatTriggers: Trigger[] = [];
|
||||||
|
|
||||||
|
triggers.forEach((t) => {
|
||||||
|
if ("triggers" in t) {
|
||||||
|
if (t.triggers) {
|
||||||
|
flatTriggers.push(...ensureArray(t.triggers));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flatTriggers.push(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flatTriggers;
|
||||||
|
};
|
||||||
|
|
||||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||||
initialAutomationEditorData = data;
|
initialAutomationEditorData = data;
|
||||||
navigate("/config/automation/edit/new");
|
navigate("/config/automation/edit/new");
|
||||||
|
@@ -68,9 +68,18 @@ export const describeTrigger = (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
) => {
|
): string => {
|
||||||
try {
|
try {
|
||||||
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
|
const description = tryDescribeTrigger(
|
||||||
|
trigger,
|
||||||
|
hass,
|
||||||
|
entityRegistry,
|
||||||
|
ignoreAlias
|
||||||
|
);
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
throw new Error(String(description));
|
||||||
|
}
|
||||||
|
return description;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -700,9 +709,18 @@ export const describeCondition = (
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryEntry[],
|
entityRegistry: EntityRegistryEntry[],
|
||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
) => {
|
): string => {
|
||||||
try {
|
try {
|
||||||
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
|
const description = tryDescribeCondition(
|
||||||
|
condition,
|
||||||
|
hass,
|
||||||
|
entityRegistry,
|
||||||
|
ignoreAlias
|
||||||
|
);
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
throw new Error(String(description));
|
||||||
|
}
|
||||||
|
return description;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { IntegrationManifest, IntegrationType } from "./integration";
|
import type { IntegrationType } from "./integration";
|
||||||
|
|
||||||
export interface ConfigEntry {
|
export interface ConfigEntry {
|
||||||
entry_id: string;
|
entry_id: string;
|
||||||
@@ -149,20 +149,19 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
|
|||||||
|
|
||||||
export const sortConfigEntries = (
|
export const sortConfigEntries = (
|
||||||
configEntries: ConfigEntry[],
|
configEntries: ConfigEntry[],
|
||||||
manifestLookup: { [domain: string]: IntegrationManifest }
|
primaryConfigEntry: string | null
|
||||||
): ConfigEntry[] => {
|
): ConfigEntry[] => {
|
||||||
const sortedConfigEntries = [...configEntries];
|
if (!primaryConfigEntry) {
|
||||||
|
return configEntries;
|
||||||
const getScore = (entry: ConfigEntry) => {
|
}
|
||||||
const manifest = manifestLookup[entry.domain] as
|
const primaryEntry = configEntries.find(
|
||||||
| IntegrationManifest
|
(e) => e.entry_id === primaryConfigEntry
|
||||||
| undefined;
|
);
|
||||||
const isHelper = manifest?.integration_type === "helper";
|
if (!primaryEntry) {
|
||||||
return isHelper ? -1 : 1;
|
return configEntries;
|
||||||
};
|
}
|
||||||
|
const otherEntries = configEntries.filter(
|
||||||
const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
|
(e) => e.entry_id !== primaryConfigEntry
|
||||||
getScore(b) - getScore(a);
|
);
|
||||||
|
return [primaryEntry, ...otherEntries];
|
||||||
return sortedConfigEntries.sort(configEntriesCompare);
|
|
||||||
};
|
};
|
||||||
|
@@ -1,10 +1,20 @@
|
|||||||
export interface DataTableFilters {
|
export interface DataTableFilters {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
value: string[] | { key: string[] } | undefined;
|
value: DataTableFiltersValue;
|
||||||
items: Set<string> | undefined;
|
items: Set<string> | undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
|
||||||
|
|
||||||
|
export interface DataTableFiltersValues {
|
||||||
|
[key: string]: DataTableFiltersValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableFiltersItems {
|
||||||
|
[key: string]: Set<string> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const serializeFilters = (value: DataTableFilters) => {
|
export const serializeFilters = (value: DataTableFilters) => {
|
||||||
const serializedValue = {};
|
const serializedValue = {};
|
||||||
Object.entries(value).forEach(([key, val]) => {
|
Object.entries(value).forEach(([key, val]) => {
|
||||||
|
@@ -33,6 +33,7 @@ export interface DeviceRegistryEntry extends RegistryEntry {
|
|||||||
entry_type: "service" | null;
|
entry_type: "service" | null;
|
||||||
disabled_by: "user" | "integration" | "config_entry" | null;
|
disabled_by: "user" | "integration" | "config_entry" | null;
|
||||||
configuration_url: string | null;
|
configuration_url: string | null;
|
||||||
|
primary_config_entry: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceEntityDisplayLookup {
|
export interface DeviceEntityDisplayLookup {
|
||||||
|
@@ -11,6 +11,7 @@ import {
|
|||||||
isLastDayOfMonth,
|
isLastDayOfMonth,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import {
|
import {
|
||||||
calcDate,
|
calcDate,
|
||||||
calcDateProperty,
|
calcDateProperty,
|
||||||
@@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
|
|||||||
|
|
||||||
export const energyStatisticHelpUrl =
|
export const energyStatisticHelpUrl =
|
||||||
"/docs/energy/faq/#troubleshooting-missing-entities";
|
"/docs/energy/faq/#troubleshooting-missing-entities";
|
||||||
|
|
||||||
|
interface EnergySumData {
|
||||||
|
to_grid?: { [start: number]: number };
|
||||||
|
from_grid?: { [start: number]: number };
|
||||||
|
to_battery?: { [start: number]: number };
|
||||||
|
from_battery?: { [start: number]: number };
|
||||||
|
solar?: { [start: number]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EnergyConsumptionData {
|
||||||
|
total: { [start: number]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSummedData = memoizeOne(
|
||||||
|
(
|
||||||
|
data: EnergyData
|
||||||
|
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
|
||||||
|
const summedData = getSummedDataPartial(data);
|
||||||
|
const compareSummedData = data.statsCompare
|
||||||
|
? getSummedDataPartial(data, true)
|
||||||
|
: undefined;
|
||||||
|
return { summedData, compareSummedData };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSummedDataPartial = (
|
||||||
|
data: EnergyData,
|
||||||
|
compare?: boolean
|
||||||
|
): EnergySumData => {
|
||||||
|
const statIds: {
|
||||||
|
to_grid?: string[];
|
||||||
|
from_grid?: string[];
|
||||||
|
solar?: string[];
|
||||||
|
to_battery?: string[];
|
||||||
|
from_battery?: string[];
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
for (const source of data.prefs.energy_sources) {
|
||||||
|
if (source.type === "solar") {
|
||||||
|
if (statIds.solar) {
|
||||||
|
statIds.solar.push(source.stat_energy_from);
|
||||||
|
} else {
|
||||||
|
statIds.solar = [source.stat_energy_from];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type === "battery") {
|
||||||
|
if (statIds.to_battery) {
|
||||||
|
statIds.to_battery.push(source.stat_energy_to);
|
||||||
|
statIds.from_battery!.push(source.stat_energy_from);
|
||||||
|
} else {
|
||||||
|
statIds.to_battery = [source.stat_energy_to];
|
||||||
|
statIds.from_battery = [source.stat_energy_from];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.type !== "grid") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// grid source
|
||||||
|
for (const flowFrom of source.flow_from) {
|
||||||
|
if (statIds.from_grid) {
|
||||||
|
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||||
|
} else {
|
||||||
|
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const flowTo of source.flow_to) {
|
||||||
|
if (statIds.to_grid) {
|
||||||
|
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||||
|
} else {
|
||||||
|
statIds.to_grid = [flowTo.stat_energy_to];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summedData: EnergySumData = {};
|
||||||
|
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
||||||
|
const totalStats: { [start: number]: number } = {};
|
||||||
|
const sets: { [statId: string]: { [start: number]: number } } = {};
|
||||||
|
subStatIds!.forEach((id) => {
|
||||||
|
const stats = compare ? data.statsCompare[id] : data.stats[id];
|
||||||
|
if (!stats) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const set = {};
|
||||||
|
stats.forEach((stat) => {
|
||||||
|
if (stat.change === null || stat.change === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const val = stat.change;
|
||||||
|
// Get total of solar and to grid to calculate the solar energy used
|
||||||
|
totalStats[stat.start] =
|
||||||
|
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||||
|
});
|
||||||
|
sets[id] = set;
|
||||||
|
});
|
||||||
|
summedData[key] = totalStats;
|
||||||
|
});
|
||||||
|
|
||||||
|
return summedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeConsumptionData = memoizeOne(
|
||||||
|
(
|
||||||
|
data: EnergySumData,
|
||||||
|
compareData?: EnergySumData
|
||||||
|
): {
|
||||||
|
consumption: EnergyConsumptionData;
|
||||||
|
compareConsumption?: EnergyConsumptionData;
|
||||||
|
} => {
|
||||||
|
const consumption = computeConsumptionDataPartial(data);
|
||||||
|
const compareConsumption = compareData
|
||||||
|
? computeConsumptionDataPartial(compareData)
|
||||||
|
: undefined;
|
||||||
|
return { consumption, compareConsumption };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const computeConsumptionDataPartial = (
|
||||||
|
data: EnergySumData
|
||||||
|
): EnergyConsumptionData => {
|
||||||
|
const outData: EnergyConsumptionData = { total: {} };
|
||||||
|
|
||||||
|
Object.keys(data).forEach((type) => {
|
||||||
|
Object.keys(data[type]).forEach((start) => {
|
||||||
|
if (outData.total[start] === undefined) {
|
||||||
|
const consumption =
|
||||||
|
(data.from_grid?.[start] || 0) +
|
||||||
|
(data.solar?.[start] || 0) +
|
||||||
|
(data.from_battery?.[start] || 0) -
|
||||||
|
(data.to_grid?.[start] || 0) -
|
||||||
|
(data.to_battery?.[start] || 0);
|
||||||
|
outData.total[start] = consumption;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return outData;
|
||||||
|
};
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
|
||||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import { debounce } from "../common/util/debounce";
|
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { AreaRegistryEntry } from "./area_registry";
|
import { AreaRegistryEntry } from "./area_registry";
|
||||||
import { RegistryEntry } from "./registry";
|
import { RegistryEntry } from "./registry";
|
||||||
@@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
|
|||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFloorRegistry = (conn: Connection) =>
|
|
||||||
conn
|
|
||||||
.sendMessagePromise({
|
|
||||||
type: "config/floor_registry/list",
|
|
||||||
})
|
|
||||||
.then((floors) =>
|
|
||||||
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
|
||||||
if (ent1.level !== ent2.level) {
|
|
||||||
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
|
||||||
}
|
|
||||||
return stringCompare(ent1.name, ent2.name);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const subscribeFloorRegistryUpdates = (
|
|
||||||
conn: Connection,
|
|
||||||
store: Store<FloorRegistryEntry[]>
|
|
||||||
) =>
|
|
||||||
conn.subscribeEvents(
|
|
||||||
debounce(
|
|
||||||
() =>
|
|
||||||
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
|
||||||
store.setState(areas, true)
|
|
||||||
),
|
|
||||||
500,
|
|
||||||
true
|
|
||||||
),
|
|
||||||
"floor_registry_updated"
|
|
||||||
);
|
|
||||||
|
|
||||||
export const subscribeFloorRegistry = (
|
|
||||||
conn: Connection,
|
|
||||||
onChange: (floors: FloorRegistryEntry[]) => void
|
|
||||||
) =>
|
|
||||||
createCollection<FloorRegistryEntry[]>(
|
|
||||||
"_floorRegistry",
|
|
||||||
fetchFloorRegistry,
|
|
||||||
subscribeFloorRegistryUpdates,
|
|
||||||
conn,
|
|
||||||
onChange
|
|
||||||
);
|
|
||||||
|
|
||||||
export const createFloorRegistryEntry = (
|
export const createFloorRegistryEntry = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
values: FloorRegistryEntryMutableParams
|
values: FloorRegistryEntryMutableParams
|
||||||
|
@@ -62,7 +62,7 @@ export interface ComponentIcons {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceIcons {
|
interface ServiceIcons {
|
||||||
[service: string]: string;
|
[service: string]: { service: string; sections?: { [name: string]: string } };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IconCategory = "entity" | "entity_component" | "services";
|
export type IconCategory = "entity" | "entity_component" | "services";
|
||||||
@@ -288,7 +288,8 @@ export const serviceIcon = async (
|
|||||||
const serviceName = computeObjectId(service);
|
const serviceName = computeObjectId(service);
|
||||||
const serviceIcons = await getServiceIcons(hass, domain);
|
const serviceIcons = await getServiceIcons(hass, domain);
|
||||||
if (serviceIcons) {
|
if (serviceIcons) {
|
||||||
icon = serviceIcons[serviceName] as string;
|
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
|
||||||
|
icon = srvceIcon?.service;
|
||||||
}
|
}
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
icon = await domainIcon(hass, domain);
|
icon = await domainIcon(hass, domain);
|
||||||
@@ -296,6 +297,21 @@ export const serviceIcon = async (
|
|||||||
return icon;
|
return icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const serviceSectionIcon = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
service: string,
|
||||||
|
section: string
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const domain = computeDomain(service);
|
||||||
|
const serviceName = computeObjectId(service);
|
||||||
|
const serviceIcons = await getServiceIcons(hass, domain);
|
||||||
|
if (serviceIcons) {
|
||||||
|
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
|
||||||
|
return srvceIcon?.sections?.[section];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const domainIcon = async (
|
export const domainIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
domain: string,
|
domain: string,
|
||||||
|
@@ -13,7 +13,7 @@ export const ensureBadgeConfig = (
|
|||||||
return {
|
return {
|
||||||
type: "entity",
|
type: "entity",
|
||||||
entity: config,
|
entity: config,
|
||||||
display_type: "complete",
|
show_name: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if ("type" in config && config.type) {
|
if ("type" in config && config.type) {
|
||||||
|
@@ -5,6 +5,8 @@ import type { LovelaceStrategyConfig } from "./strategy";
|
|||||||
export interface LovelaceBaseSectionConfig {
|
export interface LovelaceBaseSectionConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
visibility?: Condition[];
|
visibility?: Condition[];
|
||||||
|
column_span?: number;
|
||||||
|
row_span?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||||
|
@@ -22,7 +22,9 @@ export interface LovelaceBaseViewConfig {
|
|||||||
visible?: boolean | ShowViewConfig[];
|
visible?: boolean | ShowViewConfig[];
|
||||||
subview?: boolean;
|
subview?: boolean;
|
||||||
back_path?: string;
|
back_path?: string;
|
||||||
max_columns?: number; // Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||||
|
max_columns?: number;
|
||||||
|
dense_section_placement?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||||
|
@@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|||||||
import { navigate } from "../common/navigate";
|
import { navigate } from "../common/navigate";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { subscribeDeviceRegistry } from "./device_registry";
|
import { subscribeDeviceRegistry } from "./device_registry";
|
||||||
|
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||||
|
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||||
|
|
||||||
export enum NetworkType {
|
export enum NetworkType {
|
||||||
THREAD = "thread",
|
THREAD = "thread",
|
||||||
@@ -51,10 +53,30 @@ export interface MatterCommissioningParameters {
|
|||||||
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
|
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
|
||||||
hass.auth.external?.config.canCommissionMatter;
|
hass.auth.external?.config.canCommissionMatter;
|
||||||
|
|
||||||
export const startExternalCommissioning = (hass: HomeAssistant) =>
|
export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||||
hass.auth.external!.fireMessage({
|
if (isComponentLoaded(hass, "thread")) {
|
||||||
|
const datasets = await listThreadDataSets(hass);
|
||||||
|
const preferredDataset = datasets.datasets.find(
|
||||||
|
(dataset) => dataset.preferred
|
||||||
|
);
|
||||||
|
if (preferredDataset) {
|
||||||
|
return hass.auth.external!.fireMessage({
|
||||||
|
type: "matter/commission",
|
||||||
|
payload: {
|
||||||
|
active_operational_dataset: (
|
||||||
|
await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
|
||||||
|
).tlv,
|
||||||
|
border_agent_id: preferredDataset.preferred_border_agent_id,
|
||||||
|
mac_extended_address: preferredDataset.preferred_extended_address,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hass.auth.external!.fireMessage({
|
||||||
type: "matter/commission",
|
type: "matter/commission",
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const redirectOnNewMatterDevice = (
|
export const redirectOnNewMatterDevice = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@@ -50,7 +50,7 @@ export const describeAction = <T extends ActionType>(
|
|||||||
ignoreAlias = false
|
ignoreAlias = false
|
||||||
): string => {
|
): string => {
|
||||||
try {
|
try {
|
||||||
return tryDescribeAction(
|
const description = tryDescribeAction(
|
||||||
hass,
|
hass,
|
||||||
entityRegistry,
|
entityRegistry,
|
||||||
labelRegistry,
|
labelRegistry,
|
||||||
@@ -59,6 +59,10 @@ export const describeAction = <T extends ActionType>(
|
|||||||
actionType,
|
actionType,
|
||||||
ignoreAlias
|
ignoreAlias
|
||||||
);
|
);
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
throw new Error(String(description));
|
||||||
|
}
|
||||||
|
return description;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -127,6 +131,12 @@ const tryDescribeAction = <T extends ActionType>(
|
|||||||
targets.push(
|
targets.push(
|
||||||
computeEntityRegistryName(hass, entityReg) || targetThing
|
computeEntityRegistryName(hass, entityReg) || targetThing
|
||||||
);
|
);
|
||||||
|
} else if (targetThing === "all") {
|
||||||
|
targets.push(
|
||||||
|
hass.localize(
|
||||||
|
`${actionTranslationBaseKey}.service.description.target_every_entity`
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
targets.push(
|
targets.push(
|
||||||
hass.localize(
|
hass.localize(
|
||||||
|
@@ -101,9 +101,8 @@ export interface AttributeSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BooleanSelector {
|
export interface BooleanSelector {
|
||||||
boolean: {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
mode?: "checkbox" | "switch";
|
boolean: {} | null;
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColorRGBSelector {
|
export interface ColorRGBSelector {
|
||||||
@@ -324,6 +323,7 @@ export interface NumberSelector {
|
|||||||
step?: number | "any";
|
step?: number | "any";
|
||||||
mode?: "box" | "slider";
|
mode?: "box" | "slider";
|
||||||
unit_of_measurement?: string;
|
unit_of_measurement?: string;
|
||||||
|
slider_ticks?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,8 +428,7 @@ export interface ThemeSelector {
|
|||||||
theme: { include_default?: boolean } | null;
|
theme: { include_default?: boolean } | null;
|
||||||
}
|
}
|
||||||
export interface TimeSelector {
|
export interface TimeSelector {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
time: { no_second?: boolean } | null;
|
||||||
time: {} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TriggerSelector {
|
export interface TriggerSelector {
|
||||||
|
@@ -21,6 +21,8 @@ export interface SpeechMetadata {
|
|||||||
export interface STTEngine {
|
export interface STTEngine {
|
||||||
engine_id: string;
|
engine_id: string;
|
||||||
supported_languages?: string[];
|
supported_languages?: string[];
|
||||||
|
name?: string;
|
||||||
|
deprecated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listSTTEngines = (
|
export const listSTTEngines = (
|
||||||
|
@@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
|
|||||||
import {
|
import {
|
||||||
BlueprintAutomationConfig,
|
BlueprintAutomationConfig,
|
||||||
ManualAutomationConfig,
|
ManualAutomationConfig,
|
||||||
|
flattenTriggers,
|
||||||
} from "./automation";
|
} from "./automation";
|
||||||
import { BlueprintScriptConfig, ScriptConfig } from "./script";
|
import { BlueprintScriptConfig, ScriptConfig } from "./script";
|
||||||
|
|
||||||
@@ -190,7 +191,11 @@ export const getDataFromPath = (
|
|||||||
if (!tempResult && raw === "sequence") {
|
if (!tempResult && raw === "sequence") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
result = tempResult;
|
if (raw === "trigger") {
|
||||||
|
result = flattenTriggers(tempResult);
|
||||||
|
} else {
|
||||||
|
result = tempResult;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,8 @@ import { HomeAssistant } from "../types";
|
|||||||
export interface TTSEngine {
|
export interface TTSEngine {
|
||||||
engine_id: string;
|
engine_id: string;
|
||||||
supported_languages?: string[];
|
supported_languages?: string[];
|
||||||
|
name?: string;
|
||||||
|
deprecated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TTSVoice {
|
export interface TTSVoice {
|
||||||
|
47
src/data/ws-floor_registry.ts
Normal file
47
src/data/ws-floor_registry.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||||
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { debounce } from "../common/util/debounce";
|
||||||
|
import { FloorRegistryEntry } from "./floor_registry";
|
||||||
|
|
||||||
|
const fetchFloorRegistry = (conn: Connection) =>
|
||||||
|
conn
|
||||||
|
.sendMessagePromise({
|
||||||
|
type: "config/floor_registry/list",
|
||||||
|
})
|
||||||
|
.then((floors) =>
|
||||||
|
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
||||||
|
if (ent1.level !== ent2.level) {
|
||||||
|
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
||||||
|
}
|
||||||
|
return stringCompare(ent1.name, ent2.name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const subscribeFloorRegistryUpdates = (
|
||||||
|
conn: Connection,
|
||||||
|
store: Store<FloorRegistryEntry[]>
|
||||||
|
) =>
|
||||||
|
conn.subscribeEvents(
|
||||||
|
debounce(
|
||||||
|
() =>
|
||||||
|
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
||||||
|
store.setState(areas, true)
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
"floor_registry_updated"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeFloorRegistry = (
|
||||||
|
conn: Connection,
|
||||||
|
onChange: (floors: FloorRegistryEntry[]) => void
|
||||||
|
) =>
|
||||||
|
createCollection<FloorRegistryEntry[]>(
|
||||||
|
"_floorRegistry",
|
||||||
|
fetchFloorRegistry,
|
||||||
|
subscribeFloorRegistryUpdates,
|
||||||
|
conn,
|
||||||
|
onChange
|
||||||
|
);
|
@@ -252,6 +252,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
|
|||||||
type: string;
|
type: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
states: { [key: number]: string };
|
states: { [key: number]: string };
|
||||||
|
default: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZWaveJSSetConfigParamData {
|
export interface ZWaveJSSetConfigParamData {
|
||||||
|
@@ -76,17 +76,36 @@ export const showConfigFlowDialog = (
|
|||||||
: "";
|
: "";
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormStepFieldLabel(hass, step, field) {
|
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||||
return hass.localize(
|
if (field.type === "expandable") {
|
||||||
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
|
return hass.localize(
|
||||||
|
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${step.handler}.config.step.${step.step_id}.${prefix}data.${field.name}`
|
||||||
|
) || field.name
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormStepFieldHelper(hass, step, field) {
|
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||||
|
if (field.type === "expandable") {
|
||||||
|
return hass.localize(
|
||||||
|
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.sections.${field.name}.description`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
const description = hass.localize(
|
const description = hass.localize(
|
||||||
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
|
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||||
step.description_placeholders
|
step.description_placeholders
|
||||||
);
|
);
|
||||||
|
|
||||||
return description
|
return description
|
||||||
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||||
: "";
|
: "";
|
||||||
|
@@ -49,13 +49,15 @@ export interface FlowConfig {
|
|||||||
renderShowFormStepFieldLabel(
|
renderShowFormStepFieldLabel(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
step: DataEntryFlowStepForm,
|
step: DataEntryFlowStepForm,
|
||||||
field: HaFormSchema
|
field: HaFormSchema,
|
||||||
|
options: { path?: string[]; [key: string]: any }
|
||||||
): string;
|
): string;
|
||||||
|
|
||||||
renderShowFormStepFieldHelper(
|
renderShowFormStepFieldHelper(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
step: DataEntryFlowStepForm,
|
step: DataEntryFlowStepForm,
|
||||||
field: HaFormSchema
|
field: HaFormSchema,
|
||||||
|
options: { path?: string[]; [key: string]: any }
|
||||||
): TemplateResult | string;
|
): TemplateResult | string;
|
||||||
|
|
||||||
renderShowFormStepFieldError(
|
renderShowFormStepFieldError(
|
||||||
|
@@ -93,15 +93,33 @@ export const showOptionsFlowDialog = (
|
|||||||
: "";
|
: "";
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormStepFieldLabel(hass, step, field) {
|
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||||
return hass.localize(
|
if (field.type === "expandable") {
|
||||||
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
|
return hass.localize(
|
||||||
|
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
hass.localize(
|
||||||
|
`component.${configEntry.domain}.options.step.${step.step_id}.${prefix}data.${field.name}`
|
||||||
|
) || field.name
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShowFormStepFieldHelper(hass, step, field) {
|
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||||
|
if (field.type === "expandable") {
|
||||||
|
return hass.localize(
|
||||||
|
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.description`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||||
|
|
||||||
const description = hass.localize(
|
const description = hass.localize(
|
||||||
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
|
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||||
step.description_placeholders
|
step.description_placeholders
|
||||||
);
|
);
|
||||||
return description
|
return description
|
||||||
|
@@ -225,11 +225,24 @@ class StepFlowForm extends LitElement {
|
|||||||
this._stepData = ev.detail.value;
|
this._stepData = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _labelCallback = (field: HaFormSchema): string =>
|
private _labelCallback = (field: HaFormSchema, _data, options): string =>
|
||||||
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
|
this.flowConfig.renderShowFormStepFieldLabel(
|
||||||
|
this.hass,
|
||||||
|
this.step,
|
||||||
|
field,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
private _helperCallback = (field: HaFormSchema): string | TemplateResult =>
|
private _helperCallback = (
|
||||||
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
|
field: HaFormSchema,
|
||||||
|
options
|
||||||
|
): string | TemplateResult =>
|
||||||
|
this.flowConfig.renderShowFormStepFieldHelper(
|
||||||
|
this.hass,
|
||||||
|
this.step,
|
||||||
|
field,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
private _errorCallback = (error: string) =>
|
private _errorCallback = (error: string) =>
|
||||||
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
|
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
|
||||||
import { mdiAlertOutline } from "@mdi/js";
|
import { mdiAlertOutline } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||||
|
import "../../components/ha-dialog-header";
|
||||||
import "../../components/ha-svg-icon";
|
import "../../components/ha-svg-icon";
|
||||||
import "../../components/ha-switch";
|
import "../../components/ha-button";
|
||||||
import { HaTextField } from "../../components/ha-textfield";
|
import { HaTextField } from "../../components/ha-textfield";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { DialogBoxParams } from "./show-dialog-box";
|
import { DialogBoxParams } from "./show-dialog-box";
|
||||||
@@ -18,8 +19,12 @@ class DialogBox extends LitElement {
|
|||||||
|
|
||||||
@state() private _params?: DialogBoxParams;
|
@state() private _params?: DialogBoxParams;
|
||||||
|
|
||||||
|
@state() private _closeState?: "canceled" | "confirmed";
|
||||||
|
|
||||||
@query("ha-textfield") private _textField?: HaTextField;
|
@query("ha-textfield") private _textField?: HaTextField;
|
||||||
|
|
||||||
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
public async showDialog(params: DialogBoxParams): Promise<void> {
|
public async showDialog(params: DialogBoxParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
}
|
}
|
||||||
@@ -42,33 +47,33 @@ class DialogBox extends LitElement {
|
|||||||
|
|
||||||
const confirmPrompt = this._params.confirmation || this._params.prompt;
|
const confirmPrompt = this._params.confirmation || this._params.prompt;
|
||||||
|
|
||||||
|
const dialogTitle =
|
||||||
|
this._params.title ||
|
||||||
|
(this._params.confirmation &&
|
||||||
|
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-md-dialog
|
||||||
open
|
open
|
||||||
?scrimClickAction=${confirmPrompt}
|
.disableCancelAction=${confirmPrompt || false}
|
||||||
?escapeKeyAction=${confirmPrompt}
|
|
||||||
@closed=${this._dialogClosed}
|
@closed=${this._dialogClosed}
|
||||||
defaultAction="ignore"
|
type="alert"
|
||||||
.heading=${html`${this._params.warning
|
aria-labelledby="dialog-box-title"
|
||||||
? html`<ha-svg-icon
|
aria-describedby="dialog-box-description"
|
||||||
.path=${mdiAlertOutline}
|
|
||||||
style="color: var(--warning-color)"
|
|
||||||
></ha-svg-icon> `
|
|
||||||
: ""}${this._params.title
|
|
||||||
? this._params.title
|
|
||||||
: this._params.confirmation &&
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.dialogs.generic.default_confirmation_title"
|
|
||||||
)}`}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div slot="headline">
|
||||||
${this._params.text
|
<span .title=${dialogTitle} id="dialog-box-title">
|
||||||
? html`
|
${this._params.warning
|
||||||
<p class=${this._params.prompt ? "no-bottom-padding" : ""}>
|
? html`<ha-svg-icon
|
||||||
${this._params.text}
|
.path=${mdiAlertOutline}
|
||||||
</p>
|
style="color: var(--warning-color)"
|
||||||
`
|
></ha-svg-icon> `
|
||||||
: ""}
|
: nothing}
|
||||||
|
${dialogTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div slot="content" id="dialog-box-description">
|
||||||
|
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||||
${this._params.prompt
|
${this._params.prompt
|
||||||
? html`
|
? html`
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
@@ -87,58 +92,64 @@ class DialogBox extends LitElement {
|
|||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
${confirmPrompt &&
|
<div slot="actions">
|
||||||
html`
|
${confirmPrompt &&
|
||||||
<mwc-button
|
html`
|
||||||
@click=${this._dismiss}
|
<ha-button
|
||||||
slot="secondaryAction"
|
@click=${this._dismiss}
|
||||||
|
?dialogInitialFocus=${!this._params.prompt &&
|
||||||
|
this._params.destructive}
|
||||||
|
>
|
||||||
|
${this._params.dismissText
|
||||||
|
? this._params.dismissText
|
||||||
|
: this.hass.localize("ui.dialogs.generic.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
`}
|
||||||
|
<ha-button
|
||||||
|
@click=${this._confirm}
|
||||||
?dialogInitialFocus=${!this._params.prompt &&
|
?dialogInitialFocus=${!this._params.prompt &&
|
||||||
this._params.destructive}
|
!this._params.destructive}
|
||||||
|
class=${classMap({
|
||||||
|
destructive: this._params.destructive || false,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
${this._params.dismissText
|
${this._params.confirmText
|
||||||
? this._params.dismissText
|
? this._params.confirmText
|
||||||
: this.hass.localize("ui.dialogs.generic.cancel")}
|
: this.hass.localize("ui.dialogs.generic.ok")}
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
`}
|
</div>
|
||||||
<mwc-button
|
</ha-md-dialog>
|
||||||
@click=${this._confirm}
|
|
||||||
?dialogInitialFocus=${!this._params.prompt &&
|
|
||||||
!this._params.destructive}
|
|
||||||
slot="primaryAction"
|
|
||||||
class=${classMap({
|
|
||||||
destructive: this._params.destructive || false,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${this._params.confirmText
|
|
||||||
? this._params.confirmText
|
|
||||||
: this.hass.localize("ui.dialogs.generic.ok")}
|
|
||||||
</mwc-button>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dismiss(): void {
|
private _cancel(): void {
|
||||||
if (this._params?.cancel) {
|
if (this._params?.cancel) {
|
||||||
this._params.cancel();
|
this._params.cancel();
|
||||||
}
|
}
|
||||||
this._close();
|
}
|
||||||
|
|
||||||
|
private _dismiss(): void {
|
||||||
|
this._cancel();
|
||||||
|
this._closeState = "canceled";
|
||||||
|
this._closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _confirm(): void {
|
private _confirm(): void {
|
||||||
if (this._params!.confirm) {
|
if (this._params!.confirm) {
|
||||||
this._params!.confirm(this._textField?.value);
|
this._params!.confirm(this._textField?.value);
|
||||||
}
|
}
|
||||||
this._close();
|
this._closeState = "confirmed";
|
||||||
|
this._closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogClosed(ev) {
|
private _closeDialog() {
|
||||||
if (ev.detail.action === "ignore") {
|
this._dialog?.close();
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
private _dialogClosed() {
|
||||||
|
if (!this._closeState) {
|
||||||
|
this._cancel();
|
||||||
}
|
}
|
||||||
this._dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _close(): void {
|
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -168,10 +179,6 @@ class DialogBox extends LitElement {
|
|||||||
.destructive {
|
.destructive {
|
||||||
--mdc-theme-primary: var(--error-color);
|
--mdc-theme-primary: var(--error-color);
|
||||||
}
|
}
|
||||||
ha-dialog {
|
|
||||||
/* Place above other dialogs */
|
|
||||||
--dialog-z-index: 104;
|
|
||||||
}
|
|
||||||
@media all and (min-width: 600px) {
|
@media all and (min-width: 600px) {
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--mdc-dialog-min-width: 400px;
|
--mdc-dialog-min-width: 400px;
|
||||||
|
@@ -1,15 +1,18 @@
|
|||||||
import { mdiClose } from "@mdi/js";
|
import { mdiClose } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state, query } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
import "../../../../components/ha-dialog";
|
import {
|
||||||
|
getMobileOpenFromBottomAnimation,
|
||||||
|
getMobileCloseToBottomAnimation,
|
||||||
|
} from "../../../../components/ha-md-dialog";
|
||||||
|
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-header";
|
||||||
import "../../../../components/ha-icon-button-toggle";
|
import "../../../../components/ha-icon-button-toggle";
|
||||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
formatTempColor,
|
|
||||||
LightColor,
|
LightColor,
|
||||||
LightColorMode,
|
LightColorMode,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
@@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
|
|
||||||
@state() private _modes: LightPickerMode[] = [];
|
@state() private _modes: LightPickerMode[] = [];
|
||||||
|
|
||||||
@state() private _currentValue?: string;
|
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||||
|
|
||||||
private _colorHovered(ev: CustomEvent<HASSDomEvents["color-hovered"]>) {
|
|
||||||
if (ev.detail && "color_temp_kelvin" in ev.detail) {
|
|
||||||
this._currentValue = formatTempColor(ev.detail.color_temp_kelvin);
|
|
||||||
} else {
|
|
||||||
this._currentValue = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
dialogParams: LightColorFavoriteDialogParams
|
dialogParams: LightColorFavoriteDialogParams
|
||||||
@@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): void {
|
public closeDialog(): void {
|
||||||
this._dialogParams = undefined;
|
this._dialog?.close();
|
||||||
this._entry = undefined;
|
|
||||||
this._color = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateModes() {
|
private _updateModes() {
|
||||||
@@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
|
|
||||||
private async _cancel() {
|
private async _cancel() {
|
||||||
this._dialogParams?.cancel?.();
|
this._dialogParams?.cancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cancelDialog() {
|
||||||
|
this._cancel();
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _dialogClosed(): void {
|
||||||
|
this._dialogParams = undefined;
|
||||||
|
this._entry = undefined;
|
||||||
|
this._color = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
private async _save() {
|
private async _save() {
|
||||||
if (!this._color) {
|
if (!this._color) {
|
||||||
this._cancel();
|
this._cancel();
|
||||||
@@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-md-dialog
|
||||||
open
|
open
|
||||||
@closed=${this._cancel}
|
@cancel=${this._cancel}
|
||||||
.heading=${this._dialogParams?.title ?? ""}
|
@closed=${this._dialogClosed}
|
||||||
flexContent
|
aria-labelledby="dialog-light-color-favorite-title"
|
||||||
|
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||||
|
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="heading">
|
<ha-dialog-header slot="headline">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="navigationIcon"
|
slot="navigationIcon"
|
||||||
dialogAction="cancel"
|
@click=${this.closeDialog}
|
||||||
.label=${this.hass.localize("ui.common.close")}
|
.label=${this.hass.localize("ui.common.close")}
|
||||||
.path=${mdiClose}
|
.path=${mdiClose}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<span slot="title">${this._dialogParams?.title}</span>
|
<span slot="title" id="dialog-light-color-favorite-title"
|
||||||
|
>${this._dialogParams?.title}</span
|
||||||
|
>
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
<div class="header">
|
<div slot="content">
|
||||||
<span class="value">${this._currentValue}</span>
|
<div class="header">
|
||||||
${this._modes.length > 1
|
${this._modes.length > 1
|
||||||
? html`
|
? html`
|
||||||
<div class="modes">
|
<div class="modes">
|
||||||
${this._modes.map(
|
${this._modes.map(
|
||||||
(value) => html`
|
(value) => html`
|
||||||
<ha-icon-button-toggle
|
<ha-icon-button-toggle
|
||||||
border-only
|
border-only
|
||||||
.selected=${value === this._mode}
|
.selected=${value === this._mode}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||||
)}
|
)}
|
||||||
.mode=${value}
|
.mode=${value}
|
||||||
@click=${this._modeChanged}
|
@click=${this._modeChanged}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="wheel ${classMap({ [value]: true })}"
|
class="wheel ${classMap({ [value]: true })}"
|
||||||
></span>
|
></span>
|
||||||
</ha-icon-button-toggle>
|
</ha-icon-button-toggle>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
${this._mode === "color_temp"
|
||||||
|
? html`
|
||||||
|
<light-color-temp-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
@color-changed=${this._colorChanged}
|
||||||
|
>
|
||||||
|
</light-color-temp-picker>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this._mode === "color"
|
||||||
|
? html`
|
||||||
|
<light-color-rgb-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
@color-changed=${this._colorChanged}
|
||||||
|
>
|
||||||
|
</light-color-rgb-picker>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
<div class="content">
|
<ha-button @click=${this._cancelDialog}>
|
||||||
${this._mode === "color_temp"
|
${this.hass.localize("ui.common.cancel")}
|
||||||
? html`
|
</ha-button>
|
||||||
<light-color-temp-picker
|
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||||
.hass=${this.hass}
|
>${this.hass.localize("ui.common.save")}</ha-button
|
||||||
.stateObj=${this.stateObj}
|
>
|
||||||
@color-changed=${this._colorChanged}
|
|
||||||
@color-hovered=${this._colorHovered}
|
|
||||||
>
|
|
||||||
</light-color-temp-picker>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this._mode === "color"
|
|
||||||
? html`
|
|
||||||
<light-color-rgb-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${this.stateObj}
|
|
||||||
@color-changed=${this._colorChanged}
|
|
||||||
@color-hovered=${this._colorHovered}
|
|
||||||
>
|
|
||||||
</light-color-rgb-picker>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
</div>
|
||||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
</ha-md-dialog>
|
||||||
${this.hass.localize("ui.common.cancel")}
|
|
||||||
</ha-button>
|
|
||||||
<ha-button
|
|
||||||
slot="primaryAction"
|
|
||||||
@click=${this._save}
|
|
||||||
.disabled=${!this._color}
|
|
||||||
>${this.hass.localize("ui.common.save")}</ha-button
|
|
||||||
>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
ha-dialog {
|
ha-md-dialog {
|
||||||
--dialog-content-padding: 0;
|
min-width: 420px; /* prevent width jumps when switching modes */
|
||||||
|
max-height: min(
|
||||||
|
600px,
|
||||||
|
100% - 48px
|
||||||
|
); /* prevent scrolling on desktop */
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
ha-dialog {
|
ha-md-dialog {
|
||||||
--dialog-surface-margin-top: 100px;
|
min-width: 100%;
|
||||||
--mdc-dialog-min-height: auto;
|
min-height: auto;
|
||||||
--mdc-dialog-max-height: calc(100% - 100px);
|
max-height: calc(100% - 100px);
|
||||||
--ha-dialog-border-radius: var(
|
margin-bottom: 0;
|
||||||
--ha-dialog-bottom-sheet-border-radius,
|
|
||||||
28px 28px 0 0
|
--md-dialog-container-shape-start-start: 28px;
|
||||||
);
|
--md-dialog-container-shape-start-end: 28px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,21 +295,6 @@ class DialogLightColorFavorite extends LitElement {
|
|||||||
rgb(255, 160, 0) 100%
|
rgb(255, 160, 0) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.value {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
height: 48px;
|
|
||||||
line-height: 48px;
|
|
||||||
letter-spacing: 0.1px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ import { isUnavailableState } from "../../../data/entity";
|
|||||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||||
import { listenMediaQuery } from "../../../common/dom/media_query";
|
import { listenMediaQuery } from "../../../common/dom/media_query";
|
||||||
import "../components/ha-more-info-state-header";
|
import "../components/ha-more-info-state-header";
|
||||||
|
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
|
|
||||||
@customElement("more-info-script")
|
@customElement("more-info-script")
|
||||||
class MoreInfoScript extends LitElement {
|
class MoreInfoScript extends LitElement {
|
||||||
@@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public stateObj?: ScriptEntity;
|
@property({ attribute: false }) public stateObj?: ScriptEntity;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
|
||||||
|
|
||||||
@state() private _scriptData: Record<string, any> = {};
|
@state() private _scriptData: Record<string, any> = {};
|
||||||
|
|
||||||
@state() private narrow = false;
|
@state() private narrow = false;
|
||||||
@@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
|
|||||||
const stateObj = this.stateObj;
|
const stateObj = this.stateObj;
|
||||||
|
|
||||||
const fields =
|
const fields =
|
||||||
this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
|
this.hass.services.script[
|
||||||
?.fields;
|
this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
|
||||||
|
]?.fields;
|
||||||
|
|
||||||
const hasFields = fields && Object.keys(fields).length > 0;
|
const hasFields = fields && Object.keys(fields).length > 0;
|
||||||
|
|
||||||
@@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
|
|||||||
protected override willUpdate(changedProperties: PropertyValues): void {
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
||||||
super.willUpdate(changedProperties);
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
if (!changedProperties.has("stateObj")) {
|
if (changedProperties.has("stateObj")) {
|
||||||
return;
|
const oldState = changedProperties.get("stateObj") as
|
||||||
|
| HassEntity
|
||||||
|
| undefined;
|
||||||
|
const newState = this.stateObj;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newState &&
|
||||||
|
(!oldState || oldState.entity_id !== newState.entity_id)
|
||||||
|
) {
|
||||||
|
this._scriptData = {
|
||||||
|
action:
|
||||||
|
this.entry?.entity_id === newState.entity_id
|
||||||
|
? `script.${this.entry.unique_id}`
|
||||||
|
: newState.entity_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldState = changedProperties.get("stateObj") as
|
if (this.entry?.unique_id && changedProperties.has("entry")) {
|
||||||
| HassEntity
|
const action = `script.${this.entry?.unique_id}`;
|
||||||
| undefined;
|
if (this._scriptData?.action !== action) {
|
||||||
const newState = this.stateObj;
|
this._scriptData = { ...this._scriptData, action };
|
||||||
|
}
|
||||||
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
|
|
||||||
this._scriptData = { action: newState.entity_id, data: {} };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.hass.callService(
|
this.hass.callService(
|
||||||
"script",
|
"script",
|
||||||
computeObjectId(this.stateObj!.entity_id),
|
this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
|
||||||
this._scriptData.data
|
this._scriptData.data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
629
src/dialogs/voice-command-dialog/assist-chat.ts
Normal file
629
src/dialogs/voice-command-dialog/assist-chat.ts
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import "../../components/ha-icon-button";
|
||||||
|
import "../../components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../../components/ha-textfield";
|
||||||
|
import {
|
||||||
|
AssistPipeline,
|
||||||
|
getAssistPipeline,
|
||||||
|
runAssistPipeline,
|
||||||
|
} from "../../data/assist_pipeline";
|
||||||
|
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;
|
||||||
|
text?: string | TemplateResult;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("assist-chat")
|
||||||
|
export class HaAssistChat extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: "pipeline-id" }) public pipelineId!: string;
|
||||||
|
|
||||||
|
@state() private _conversation?: Message[];
|
||||||
|
|
||||||
|
@state() private _pipeline?: AssistPipeline;
|
||||||
|
|
||||||
|
@state() private _showSendButton = false;
|
||||||
|
|
||||||
|
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||||
|
|
||||||
|
@query("#message-input") private _messageInput!: HaTextField;
|
||||||
|
|
||||||
|
private _conversationId: string | null = null;
|
||||||
|
|
||||||
|
private _audioRecorder?: AudioRecorder;
|
||||||
|
|
||||||
|
private _audioBuffer?: Int16Array[];
|
||||||
|
|
||||||
|
private _audio?: HTMLAudioElement;
|
||||||
|
|
||||||
|
private _stt_binary_handler_id?: number | null;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const supportsMicrophone = AudioRecorder.isSupported;
|
||||||
|
const supportsSTT = this._pipeline?.stt_engine;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="messages">
|
||||||
|
<div class="messages-container" id="scroll-container">
|
||||||
|
${this._conversation!.map(
|
||||||
|
// New lines matter for messages
|
||||||
|
// prettier-ignore
|
||||||
|
(message) => html`
|
||||||
|
<div class=${this._computeMessageClasses(message)}>${message.text}</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<ha-textfield
|
||||||
|
id="message-input"
|
||||||
|
@keyup=${this._handleKeyUp}
|
||||||
|
@input=${this._handleInput}
|
||||||
|
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
||||||
|
dialogInitialFocus
|
||||||
|
iconTrailing
|
||||||
|
>
|
||||||
|
<span slot="trailingIcon">
|
||||||
|
${this._showSendButton || !supportsSTT
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
class="listening-icon"
|
||||||
|
.path=${mdiSend}
|
||||||
|
@click=${this._handleSendMessage}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.send_text"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
${this._audioRecorder?.active
|
||||||
|
? html`
|
||||||
|
<div class="bouncer">
|
||||||
|
<div class="double-bounce1"></div>
|
||||||
|
<div class="double-bounce2"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
<div class="listening-icon">
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiMicrophone}
|
||||||
|
@click=${this._handleListeningButton}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.start_listening"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
${!supportsMicrophone
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiAlertCircle}
|
||||||
|
class="unsupported"
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</span>
|
||||||
|
</ha-textfield>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has("pipelineId")) {
|
||||||
|
this._getPipeline();
|
||||||
|
this._conversation = [
|
||||||
|
{
|
||||||
|
who: "hass",
|
||||||
|
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getPipeline() {
|
||||||
|
try {
|
||||||
|
this._pipeline = await getAssistPipeline(this.hass, this.pipelineId);
|
||||||
|
} catch (e: any) {
|
||||||
|
// Pipeline doesn't exist, we won't be able to check
|
||||||
|
// if it supports STT. We gracefully handle this.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("_conversation") || changedProps.has("results")) {
|
||||||
|
this._scrollMessagesBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addMessage(message: Message) {
|
||||||
|
this._conversation = [...this._conversation!, message];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleKeyUp(ev: KeyboardEvent) {
|
||||||
|
const input = ev.target as HaTextField;
|
||||||
|
if (ev.key === "Enter" && input.value) {
|
||||||
|
this._processText(input.value);
|
||||||
|
input.value = "";
|
||||||
|
this._showSendButton = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleInput(ev: InputEvent) {
|
||||||
|
const value = (ev.target as HaTextField).value;
|
||||||
|
if (value && !this._showSendButton) {
|
||||||
|
this._showSendButton = true;
|
||||||
|
} else if (!value && this._showSendButton) {
|
||||||
|
this._showSendButton = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSendMessage() {
|
||||||
|
if (this._messageInput.value) {
|
||||||
|
this._processText(this._messageInput.value.trim());
|
||||||
|
this._messageInput.value = "";
|
||||||
|
this._showSendButton = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _processText(text: string) {
|
||||||
|
this._audio?.pause();
|
||||||
|
this._addMessage({ who: "user", text });
|
||||||
|
const message: Message = {
|
||||||
|
who: "hass",
|
||||||
|
text: "…",
|
||||||
|
};
|
||||||
|
// To make sure the answer is placed at the right user text, we add it before we process it
|
||||||
|
this._addMessage(message);
|
||||||
|
try {
|
||||||
|
const unsub = await runAssistPipeline(
|
||||||
|
this.hass,
|
||||||
|
(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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
||||||
|
message.error = true;
|
||||||
|
this.requestUpdate("_conversation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleListeningButton(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
this.toggleListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleListening() {
|
||||||
|
const supportsMicrophone = AudioRecorder.isSupported;
|
||||||
|
if (!supportsMicrophone) {
|
||||||
|
this._showNotSupportedMessage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._audioRecorder?.active) {
|
||||||
|
this._startListening();
|
||||||
|
} else {
|
||||||
|
this.stopListening();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _showNotSupportedMessage() {
|
||||||
|
this._addMessage({
|
||||||
|
who: "hass",
|
||||||
|
text:
|
||||||
|
// New lines matter for messages
|
||||||
|
// prettier-ignore
|
||||||
|
html`${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||||
|
)}
|
||||||
|
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||||
|
{
|
||||||
|
documentation_link: html`<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href=${documentationUrl(
|
||||||
|
this.hass,
|
||||||
|
"/docs/configuration/securing/#remote-access"
|
||||||
|
)}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||||
|
)}</a>`,
|
||||||
|
}
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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._pipeline?.id,
|
||||||
|
conversation_id: this._conversationId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
await showAlertDialog(this, {
|
||||||
|
title: "Error starting pipeline",
|
||||||
|
text: err.message || err,
|
||||||
|
});
|
||||||
|
this.stopListening();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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() {
|
||||||
|
const scrollContainer = this._scrollContainer;
|
||||||
|
if (!scrollContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollContainer.scrollTo(0, 99999);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeMessageClasses(message: Message) {
|
||||||
|
return `message ${message.who} ${message.error ? " error" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
.listening-icon {
|
||||||
|
position: relative;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
margin-right: -24px;
|
||||||
|
margin-inline-end: -24px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
direction: var(--direction);
|
||||||
|
transform: scaleX(var(--scale-direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
.listening-icon[active] {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsupported {
|
||||||
|
color: var(--error-color);
|
||||||
|
position: absolute;
|
||||||
|
--mdc-icon-size: 16px;
|
||||||
|
right: 5px;
|
||||||
|
inset-inline-end: 5px;
|
||||||
|
inset-inline-start: initial;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
a.button {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.side-by-side {
|
||||||
|
display: flex;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.side-by-side > * {
|
||||||
|
flex: 1 0;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
display: block;
|
||||||
|
height: 400px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-max-width: 100%;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.messages-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
white-space: pre-line;
|
||||||
|
font-size: 18px;
|
||||||
|
clear: both;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.message p:not(:last-child) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user {
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-inline-start: 24px;
|
||||||
|
margin-inline-end: initial;
|
||||||
|
float: var(--float-end);
|
||||||
|
text-align: right;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.hass {
|
||||||
|
margin-right: 24px;
|
||||||
|
margin-inline-end: 24px;
|
||||||
|
margin-inline-start: initial;
|
||||||
|
float: var(--float-start);
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.user a {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.hass a {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouncer {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.double-bounce1,
|
||||||
|
.double-bounce2 {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
opacity: 0.2;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
||||||
|
animation: sk-bounce 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.double-bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes sk-bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes sk-bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1);
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||||
|
.message {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"assist-chat": HaAssistChat;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,8 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiHelpCircleOutline,
|
mdiHelpCircleOutline,
|
||||||
mdiMicrophone,
|
|
||||||
mdiSend,
|
|
||||||
mdiStar,
|
mdiStar,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +12,6 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
nothing,
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { storage } from "../../common/decorators/storage";
|
import { storage } from "../../common/decorators/storage";
|
||||||
@@ -27,35 +23,25 @@ import "../../components/ha-dialog";
|
|||||||
import "../../components/ha-dialog-header";
|
import "../../components/ha-dialog-header";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-textfield";
|
|
||||||
import type { HaTextField } from "../../components/ha-textfield";
|
|
||||||
import {
|
import {
|
||||||
AssistPipeline,
|
AssistPipeline,
|
||||||
getAssistPipeline,
|
getAssistPipeline,
|
||||||
listAssistPipelines,
|
listAssistPipelines,
|
||||||
runAssistPipeline,
|
|
||||||
} from "../../data/assist_pipeline";
|
} from "../../data/assist_pipeline";
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
import { haStyleDialog } from "../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { AudioRecorder } from "../../util/audio-recorder";
|
import { AudioRecorder } from "../../util/audio-recorder";
|
||||||
import { documentationUrl } from "../../util/documentation-url";
|
import { documentationUrl } from "../../util/documentation-url";
|
||||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
|
||||||
import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog";
|
import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog";
|
||||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||||
import { ConversationEntityFeature } from "../../data/conversation";
|
import { ConversationEntityFeature } from "../../data/conversation";
|
||||||
|
import "./assist-chat";
|
||||||
interface Message {
|
import type { HaAssistChat } from "./assist-chat";
|
||||||
who: string;
|
|
||||||
text?: string | TemplateResult;
|
|
||||||
error?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-voice-command-dialog")
|
@customElement("ha-voice-command-dialog")
|
||||||
export class HaVoiceCommandDialog extends LitElement {
|
export class HaVoiceCommandDialog extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _conversation?: Message[];
|
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
@@ -67,25 +53,11 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
@state() private _pipeline?: AssistPipeline;
|
@state() private _pipeline?: AssistPipeline;
|
||||||
|
|
||||||
@state() private _showSendButton = false;
|
|
||||||
|
|
||||||
@state() private _pipelines?: AssistPipeline[];
|
@state() private _pipelines?: AssistPipeline[];
|
||||||
|
|
||||||
@state() private _preferredPipeline?: string;
|
@state() private _preferredPipeline?: string;
|
||||||
|
|
||||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
@query("assist-chat") private _assistChat!: HaAssistChat;
|
||||||
|
|
||||||
@query("#message-input") private _messageInput!: HaTextField;
|
|
||||||
|
|
||||||
private _conversationId: string | null = null;
|
|
||||||
|
|
||||||
private _audioRecorder?: AudioRecorder;
|
|
||||||
|
|
||||||
private _audioBuffer?: Int16Array[];
|
|
||||||
|
|
||||||
private _audio?: HTMLAudioElement;
|
|
||||||
|
|
||||||
private _stt_binary_handler_id?: number | null;
|
|
||||||
|
|
||||||
private _pipelinePromise?: Promise<AssistPipeline>;
|
private _pipelinePromise?: Promise<AssistPipeline>;
|
||||||
|
|
||||||
@@ -101,15 +73,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
this._pipelineId = params.pipeline_id;
|
this._pipelineId = params.pipeline_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._conversation = [
|
|
||||||
{
|
|
||||||
who: "hass",
|
|
||||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
this._opened = true;
|
this._opened = true;
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
this._scrollMessagesBottom();
|
|
||||||
|
|
||||||
await this._pipelinePromise;
|
await this._pipelinePromise;
|
||||||
if (
|
if (
|
||||||
@@ -117,7 +82,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
this._pipeline?.stt_engine &&
|
this._pipeline?.stt_engine &&
|
||||||
AudioRecorder.isSupported
|
AudioRecorder.isSupported
|
||||||
) {
|
) {
|
||||||
this._toggleListening();
|
this._assistChat.toggleListening();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +90,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
this._opened = false;
|
this._opened = false;
|
||||||
this._pipeline = undefined;
|
this._pipeline = undefined;
|
||||||
this._pipelines = undefined;
|
this._pipelines = undefined;
|
||||||
this._conversation = undefined;
|
this._assistChat.stopListening();
|
||||||
this._conversationId = null;
|
|
||||||
this._audioRecorder?.close();
|
|
||||||
this._audioRecorder = undefined;
|
|
||||||
this._audio?.pause();
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,15 +107,13 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
ConversationEntityFeature.CONTROL
|
ConversationEntityFeature.CONTROL
|
||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
const supportsMicrophone = AudioRecorder.isSupported;
|
|
||||||
const supportsSTT = this._pipeline?.stt_engine;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
open
|
open
|
||||||
@closed=${this.closeDialog}
|
@closed=${this.closeDialog}
|
||||||
.heading=${this.hass.localize("ui.dialogs.voice_command.title")}
|
.heading=${this.hass.localize("ui.dialogs.voice_command.title")}
|
||||||
flexContent
|
hideactions
|
||||||
>
|
>
|
||||||
<ha-dialog-header slot="heading">
|
<ha-dialog-header slot="heading">
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@@ -231,71 +190,10 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</ha-alert>
|
</ha-alert>
|
||||||
`}
|
`}
|
||||||
<div class="messages">
|
<assist-chat
|
||||||
<div class="messages-container" id="scroll-container">
|
.hass=${this.hass}
|
||||||
${this._conversation!.map(
|
.pipelineId=${this._pipelineId}
|
||||||
// New lines matter for messages
|
></assist-chat>
|
||||||
// prettier-ignore
|
|
||||||
(message) => html`
|
|
||||||
<div class=${this._computeMessageClasses(message)}>${message.text}</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="input" slot="primaryAction">
|
|
||||||
<ha-textfield
|
|
||||||
id="message-input"
|
|
||||||
@keyup=${this._handleKeyUp}
|
|
||||||
@input=${this._handleInput}
|
|
||||||
.label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
|
|
||||||
dialogInitialFocus
|
|
||||||
iconTrailing
|
|
||||||
>
|
|
||||||
<span slot="trailingIcon">
|
|
||||||
${this._showSendButton || !supportsSTT
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
|
||||||
class="listening-icon"
|
|
||||||
.path=${mdiSend}
|
|
||||||
@click=${this._handleSendMessage}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.dialogs.voice_command.send_text"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
${this._audioRecorder?.active
|
|
||||||
? html`
|
|
||||||
<div class="bouncer">
|
|
||||||
<div class="double-bounce1"></div>
|
|
||||||
<div class="double-bounce2"></div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
|
|
||||||
<div class="listening-icon">
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiMicrophone}
|
|
||||||
@click=${this._handleListeningButton}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.dialogs.voice_command.start_listening"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
${!supportsMicrophone
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiAlertCircle}
|
|
||||||
class="unsupported"
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</span>
|
|
||||||
</ha-textfield>
|
|
||||||
</div>
|
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -333,339 +231,12 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
|
|
||||||
private async _selectPipeline(ev: CustomEvent) {
|
private async _selectPipeline(ev: CustomEvent) {
|
||||||
this._pipelineId = (ev.currentTarget as any).pipeline;
|
this._pipelineId = (ev.currentTarget as any).pipeline;
|
||||||
this._conversation = [
|
|
||||||
{
|
|
||||||
who: "hass",
|
|
||||||
text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await this.updateComplete;
|
|
||||||
this._scrollMessagesBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
if (changedProps.has("_conversation") || changedProps.has("results")) {
|
|
||||||
this._scrollMessagesBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addMessage(message: Message) {
|
|
||||||
this._conversation = [...this._conversation!, message];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleKeyUp(ev: KeyboardEvent) {
|
|
||||||
const input = ev.target as HaTextField;
|
|
||||||
if (ev.key === "Enter" && input.value) {
|
|
||||||
this._processText(input.value);
|
|
||||||
input.value = "";
|
|
||||||
this._showSendButton = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleInput(ev: InputEvent) {
|
|
||||||
const value = (ev.target as HaTextField).value;
|
|
||||||
if (value && !this._showSendButton) {
|
|
||||||
this._showSendButton = true;
|
|
||||||
} else if (!value && this._showSendButton) {
|
|
||||||
this._showSendButton = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSendMessage() {
|
|
||||||
if (this._messageInput.value) {
|
|
||||||
this._processText(this._messageInput.value.trim());
|
|
||||||
this._messageInput.value = "";
|
|
||||||
this._showSendButton = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _processText(text: string) {
|
|
||||||
this._audio?.pause();
|
|
||||||
this._addMessage({ who: "user", text });
|
|
||||||
const message: Message = {
|
|
||||||
who: "hass",
|
|
||||||
text: "…",
|
|
||||||
};
|
|
||||||
// To make sure the answer is placed at the right user text, we add it before we process it
|
|
||||||
this._addMessage(message);
|
|
||||||
try {
|
|
||||||
const unsub = await runAssistPipeline(
|
|
||||||
this.hass,
|
|
||||||
(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._pipeline?.id,
|
|
||||||
conversation_id: this._conversationId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
message.text = this.hass.localize("ui.dialogs.voice_command.error");
|
|
||||||
message.error = true;
|
|
||||||
this.requestUpdate("_conversation");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleListeningButton(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
this._toggleListening();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleListening() {
|
|
||||||
const supportsMicrophone = AudioRecorder.isSupported;
|
|
||||||
if (!supportsMicrophone) {
|
|
||||||
this._showNotSupportedMessage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this._audioRecorder?.active) {
|
|
||||||
this._startListening();
|
|
||||||
} else {
|
|
||||||
this._stopListening();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _showNotSupportedMessage() {
|
|
||||||
this._addMessage({
|
|
||||||
who: "hass",
|
|
||||||
text:
|
|
||||||
// New lines matter for messages
|
|
||||||
// prettier-ignore
|
|
||||||
html`${this.hass.localize(
|
|
||||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
|
||||||
)}
|
|
||||||
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
|
||||||
{
|
|
||||||
documentation_link: html`<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href=${documentationUrl(
|
|
||||||
this.hass,
|
|
||||||
"/docs/configuration/securing/#remote-access"
|
|
||||||
)}
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
|
||||||
)}</a>`,
|
|
||||||
}
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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: "…",
|
|
||||||
};
|
|
||||||
await this._audioRecorder.start();
|
|
||||||
|
|
||||||
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._pipeline?.id,
|
|
||||||
conversation_id: this._conversationId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err: any) {
|
|
||||||
await showAlertDialog(this, {
|
|
||||||
title: "Error starting pipeline",
|
|
||||||
text: err.message || err,
|
|
||||||
});
|
|
||||||
this._stopListening();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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() {
|
|
||||||
const scrollContainer = this._scrollContainer;
|
|
||||||
if (!scrollContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scrollContainer.scrollTo(0, 99999);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeMessageClasses(message: Message) {
|
|
||||||
return `message ${message.who} ${message.error ? " error" : ""}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleDialog,
|
haStyleDialog,
|
||||||
css`
|
css`
|
||||||
.listening-icon {
|
|
||||||
position: relative;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
margin-right: -24px;
|
|
||||||
margin-inline-end: -24px;
|
|
||||||
margin-inline-start: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
.listening-icon[active] {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.unsupported {
|
|
||||||
color: var(--error-color);
|
|
||||||
position: absolute;
|
|
||||||
--mdc-icon-size: 16px;
|
|
||||||
right: 5px;
|
|
||||||
inset-inline-end: 5px;
|
|
||||||
inset-inline-start: initial;
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-dialog {
|
ha-dialog {
|
||||||
--primary-action-button-flex: 1;
|
--primary-action-button-flex: 1;
|
||||||
--secondary-action-button-flex: 0;
|
--secondary-action-button-flex: 0;
|
||||||
@@ -726,158 +297,6 @@ export class HaVoiceCommandDialog extends LitElement {
|
|||||||
ha-button-menu a {
|
ha-button-menu a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
ha-textfield {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
a.button {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.button > mwc-button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.side-by-side {
|
|
||||||
display: flex;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
.side-by-side > * {
|
|
||||||
flex: 1 0;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
.messages {
|
|
||||||
display: block;
|
|
||||||
height: 400px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
ha-dialog {
|
|
||||||
--mdc-dialog-max-width: 100%;
|
|
||||||
}
|
|
||||||
.messages {
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.messages-container {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
right: 0px;
|
|
||||||
left: 0px;
|
|
||||||
padding: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
.message {
|
|
||||||
white-space: pre-line;
|
|
||||||
font-size: 18px;
|
|
||||||
clear: both;
|
|
||||||
margin: 8px 0;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
.message p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.message p:not(:last-child) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.user {
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-inline-start: 24px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
float: var(--float-end);
|
|
||||||
text-align: right;
|
|
||||||
border-bottom-right-radius: 0px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.hass {
|
|
||||||
margin-right: 24px;
|
|
||||||
margin-inline-end: 24px;
|
|
||||||
margin-inline-start: initial;
|
|
||||||
float: var(--float-start);
|
|
||||||
border-bottom-left-radius: 0px;
|
|
||||||
background-color: var(--secondary-background-color);
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.user a {
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.hass a {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.error {
|
|
||||||
background-color: var(--error-color);
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bouncer {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.double-bounce1,
|
|
||||||
.double-bounce2 {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
opacity: 0.2;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
-webkit-animation: sk-bounce 2s infinite ease-in-out;
|
|
||||||
animation: sk-bounce 2s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.double-bounce2 {
|
|
||||||
-webkit-animation-delay: -1s;
|
|
||||||
animation-delay: -1s;
|
|
||||||
}
|
|
||||||
@-webkit-keyframes sk-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes sk-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1);
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
.message {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
|||||||
import { subscribeAreaRegistry } from "../data/ws-area_registry";
|
import { subscribeAreaRegistry } from "../data/ws-area_registry";
|
||||||
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
|
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
|
||||||
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
|
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
|
||||||
|
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
|
||||||
import { subscribePanels } from "../data/ws-panels";
|
import { subscribePanels } from "../data/ws-panels";
|
||||||
import { subscribeThemes } from "../data/ws-themes";
|
import { subscribeThemes } from "../data/ws-themes";
|
||||||
import { subscribeUser } from "../data/ws-user";
|
import { subscribeUser } from "../data/ws-user";
|
||||||
@@ -117,6 +118,7 @@ window.hassConnection.then(({ conn }) => {
|
|||||||
subscribeEntityRegistryDisplay(conn, noop);
|
subscribeEntityRegistryDisplay(conn, noop);
|
||||||
subscribeDeviceRegistry(conn, noop);
|
subscribeDeviceRegistry(conn, noop);
|
||||||
subscribeAreaRegistry(conn, noop);
|
subscribeAreaRegistry(conn, noop);
|
||||||
|
subscribeFloorRegistry(conn, noop);
|
||||||
subscribeConfig(conn, noop);
|
subscribeConfig(conn, noop);
|
||||||
subscribeServices(conn, noop);
|
subscribeServices(conn, noop);
|
||||||
subscribePanels(conn, noop);
|
subscribePanels(conn, noop);
|
||||||
|
@@ -57,6 +57,11 @@ interface EMOutgoingMessageBarCodeNotify extends EMMessage {
|
|||||||
|
|
||||||
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
||||||
type: "matter/commission";
|
type: "matter/commission";
|
||||||
|
payload?: {
|
||||||
|
mac_extended_address: string | null;
|
||||||
|
border_agent_id: string | null;
|
||||||
|
active_operational_dataset: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
|
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
|
||||||
@@ -136,7 +141,7 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
|
|||||||
type: "thread/store_in_platform_keychain";
|
type: "thread/store_in_platform_keychain";
|
||||||
payload: {
|
payload: {
|
||||||
mac_extended_address: string;
|
mac_extended_address: string;
|
||||||
border_agent_id: string | null;
|
border_agent_id: string;
|
||||||
active_operational_dataset: string;
|
active_operational_dataset: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -35,10 +35,10 @@ import type {
|
|||||||
HaDataTable,
|
HaDataTable,
|
||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "../components/data-table/ha-data-table";
|
} from "../components/data-table/ha-data-table";
|
||||||
import "../components/ha-button-menu-new";
|
import "../components/ha-md-button-menu";
|
||||||
import "../components/ha-dialog";
|
import "../components/ha-dialog";
|
||||||
import { HaMenu } from "../components/ha-menu";
|
import { HaMenu } from "../components/ha-menu";
|
||||||
import "../components/ha-menu-item";
|
import "../components/ha-md-menu-item";
|
||||||
import "../components/search-input-outlined";
|
import "../components/search-input-outlined";
|
||||||
import type { HomeAssistant, Route } from "../types";
|
import type { HomeAssistant, Route } from "../types";
|
||||||
import "./hass-tabs-subpage";
|
import "./hass-tabs-subpage";
|
||||||
@@ -330,7 +330,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
"ui.components.subpage-data-table.exit_selection_mode"
|
"ui.components.subpage-data-table.exit_selection_mode"
|
||||||
)}
|
)}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-button-menu-new positioning="absolute">
|
<ha-md-button-menu positioning="absolute">
|
||||||
<ha-assist-chip
|
<ha-assist-chip
|
||||||
.label=${localize(
|
.label=${localize(
|
||||||
"ui.components.subpage-data-table.select"
|
"ui.components.subpage-data-table.select"
|
||||||
@@ -346,20 +346,26 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.path=${mdiMenuDown}
|
.path=${mdiMenuDown}
|
||||||
></ha-svg-icon
|
></ha-svg-icon
|
||||||
></ha-assist-chip>
|
></ha-assist-chip>
|
||||||
<ha-menu-item .value=${undefined} @click=${this._selectAll}>
|
<ha-md-menu-item
|
||||||
|
.value=${undefined}
|
||||||
|
@click=${this._selectAll}
|
||||||
|
>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${localize("ui.components.subpage-data-table.select_all")}
|
${localize("ui.components.subpage-data-table.select_all")}
|
||||||
</div>
|
</div>
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
<ha-menu-item .value=${undefined} @click=${this._selectNone}>
|
<ha-md-menu-item
|
||||||
|
.value=${undefined}
|
||||||
|
@click=${this._selectNone}
|
||||||
|
>
|
||||||
<div slot="headline">
|
<div slot="headline">
|
||||||
${localize(
|
${localize(
|
||||||
"ui.components.subpage-data-table.select_none"
|
"ui.components.subpage-data-table.select_none"
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
.value=${undefined}
|
.value=${undefined}
|
||||||
@click=${this._disableSelectMode}
|
@click=${this._disableSelectMode}
|
||||||
>
|
>
|
||||||
@@ -368,8 +374,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
"ui.components.subpage-data-table.close_select_mode"
|
"ui.components.subpage-data-table.close_select_mode"
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
</ha-button-menu-new>
|
</ha-md-button-menu>
|
||||||
<p>
|
<p>
|
||||||
${localize("ui.components.subpage-data-table.selected", {
|
${localize("ui.components.subpage-data-table.selected", {
|
||||||
selected: this.selected || "0",
|
selected: this.selected || "0",
|
||||||
@@ -476,27 +482,27 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
${Object.entries(this.columns).map(([id, column]) =>
|
${Object.entries(this.columns).map(([id, column]) =>
|
||||||
column.groupable
|
column.groupable
|
||||||
? html`
|
? html`
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
.value=${id}
|
.value=${id}
|
||||||
@click=${this._handleGroupBy}
|
@click=${this._handleGroupBy}
|
||||||
.selected=${id === this._groupColumn}
|
.selected=${id === this._groupColumn}
|
||||||
class=${classMap({ selected: id === this._groupColumn })}
|
class=${classMap({ selected: id === this._groupColumn })}
|
||||||
>
|
>
|
||||||
${column.title || column.label}
|
${column.title || column.label}
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
`
|
`
|
||||||
: nothing
|
: nothing
|
||||||
)}
|
)}
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
.value=${undefined}
|
.value=${undefined}
|
||||||
@click=${this._handleGroupBy}
|
@click=${this._handleGroupBy}
|
||||||
.selected=${this._groupColumn === undefined}
|
.selected=${this._groupColumn === undefined}
|
||||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||||
>
|
>
|
||||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
@click=${this._collapseAllGroups}
|
@click=${this._collapseAllGroups}
|
||||||
.disabled=${this._groupColumn === undefined}
|
.disabled=${this._groupColumn === undefined}
|
||||||
>
|
>
|
||||||
@@ -505,8 +511,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.path=${mdiUnfoldLessHorizontal}
|
.path=${mdiUnfoldLessHorizontal}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
${localize("ui.components.subpage-data-table.collapse_all_groups")}
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
@click=${this._expandAllGroups}
|
@click=${this._expandAllGroups}
|
||||||
.disabled=${this._groupColumn === undefined}
|
.disabled=${this._groupColumn === undefined}
|
||||||
>
|
>
|
||||||
@@ -515,13 +521,13 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.path=${mdiUnfoldMoreHorizontal}
|
.path=${mdiUnfoldMoreHorizontal}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
${localize("ui.components.subpage-data-table.expand_all_groups")}
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
</ha-menu>
|
</ha-menu>
|
||||||
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
||||||
${Object.entries(this.columns).map(([id, column]) =>
|
${Object.entries(this.columns).map(([id, column]) =>
|
||||||
column.sortable
|
column.sortable
|
||||||
? html`
|
? html`
|
||||||
<ha-menu-item
|
<ha-md-menu-item
|
||||||
.value=${id}
|
.value=${id}
|
||||||
@click=${this._handleSortBy}
|
@click=${this._handleSortBy}
|
||||||
keep-open
|
keep-open
|
||||||
@@ -539,7 +545,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
${column.title || column.label}
|
${column.title || column.label}
|
||||||
</ha-menu-item>
|
</ha-md-menu-item>
|
||||||
`
|
`
|
||||||
: nothing
|
: nothing
|
||||||
)}
|
)}
|
||||||
@@ -893,7 +899,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
|
|
||||||
#sort-by-anchor,
|
#sort-by-anchor,
|
||||||
#group-by-anchor,
|
#group-by-anchor,
|
||||||
ha-button-menu-new ha-assist-chip {
|
ha-md-button-menu ha-assist-chip {
|
||||||
--md-assist-chip-trailing-space: 8px;
|
--md-assist-chip-trailing-space: 8px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -13,6 +12,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
import "../components/ha-button";
|
||||||
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
|
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
|
||||||
import { subscribeConfigFlowInProgress } from "../data/config_flow";
|
import { subscribeConfigFlowInProgress } from "../data/config_flow";
|
||||||
import { domainToName } from "../data/integration";
|
import { domainToName } from "../data/integration";
|
||||||
@@ -117,6 +117,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
const foundIntegrations = domains.length;
|
const foundIntegrations = domains.length;
|
||||||
|
|
||||||
|
// there is a possibility that the user has no integrations
|
||||||
|
if (foundIntegrations === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="all-set-icon">🎉</div>
|
||||||
|
<h1>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.integration.all_set"
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.integration.lets_start"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div class="footer">
|
||||||
|
<ha-button unelevated @click=${this._finish}>
|
||||||
|
${this.onboardingLocalize(
|
||||||
|
"ui.panel.page-onboarding.integration.finish"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
if (domains.length > 12) {
|
if (domains.length > 12) {
|
||||||
domains = domains.slice(0, 11);
|
domains = domains.slice(0, 11);
|
||||||
}
|
}
|
||||||
@@ -149,11 +173,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<mwc-button unelevated @click=${this._finish}>
|
<ha-button unelevated @click=${this._finish}>
|
||||||
${this.onboardingLocalize(
|
${this.onboardingLocalize(
|
||||||
"ui.panel.page-onboarding.integration.finish"
|
"ui.panel.page-onboarding.integration.finish"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -193,6 +217,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
.all-set-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,6 @@ import {
|
|||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
LitElement,
|
LitElement,
|
||||||
@@ -15,15 +14,15 @@ import {
|
|||||||
html,
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-floor-icon";
|
import "../../../components/ha-floor-icon";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-svg-icon";
|
|
||||||
import "../../../components/ha-sortable";
|
import "../../../components/ha-sortable";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
AreaRegistryEntry,
|
AreaRegistryEntry,
|
||||||
createAreaRegistryEntry,
|
createAreaRegistryEntry,
|
||||||
@@ -34,7 +33,6 @@ import {
|
|||||||
createFloorRegistryEntry,
|
createFloorRegistryEntry,
|
||||||
deleteFloorRegistryEntry,
|
deleteFloorRegistryEntry,
|
||||||
getFloorAreaLookup,
|
getFloorAreaLookup,
|
||||||
subscribeFloorRegistry,
|
|
||||||
updateFloorRegistryEntry,
|
updateFloorRegistryEntry,
|
||||||
} from "../../../data/floor_registry";
|
} from "../../../data/floor_registry";
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +40,6 @@ import {
|
|||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-tabs-subpage";
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
@@ -57,7 +54,7 @@ const UNASSIGNED_PATH = ["__unassigned__"];
|
|||||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
||||||
|
|
||||||
@customElement("ha-config-areas-dashboard")
|
@customElement("ha-config-areas-dashboard")
|
||||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
export class HaConfigAreasDashboard extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public isWide = false;
|
@property({ type: Boolean }) public isWide = false;
|
||||||
@@ -66,14 +63,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@property({ attribute: false }) public route!: Route;
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
|
||||||
|
|
||||||
private _processAreas = memoizeOne(
|
private _processAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
areas: HomeAssistant["areas"],
|
areas: HomeAssistant["areas"],
|
||||||
devices: HomeAssistant["devices"],
|
devices: HomeAssistant["devices"],
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
floors: FloorRegistryEntry[]
|
floors: HomeAssistant["floors"]
|
||||||
) => {
|
) => {
|
||||||
const processArea = (area: AreaRegistryEntry) => {
|
const processArea = (area: AreaRegistryEntry) => {
|
||||||
let noDevicesInArea = 0;
|
let noDevicesInArea = 0;
|
||||||
@@ -109,7 +104,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
floors: floors.map((floor) => ({
|
floors: Object.values(floors).map((floor) => ({
|
||||||
...floor,
|
...floor,
|
||||||
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
||||||
})),
|
})),
|
||||||
@@ -118,26 +113,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
|
||||||
return [
|
|
||||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
|
||||||
this._floors = floors;
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const areasAndFloors =
|
const areasAndFloors =
|
||||||
!this.hass.areas ||
|
!this.hass.areas ||
|
||||||
!this.hass.devices ||
|
!this.hass.devices ||
|
||||||
!this.hass.entities ||
|
!this.hass.entities ||
|
||||||
!this._floors
|
!this.hass.floors
|
||||||
? undefined
|
? undefined
|
||||||
: this._processAreas(
|
: this._processAreas(
|
||||||
this.hass.areas,
|
this.hass.areas,
|
||||||
this.hass.devices,
|
this.hass.devices,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
this._floors
|
this.hass.floors
|
||||||
);
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -327,7 +314,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||||||
this.hass.areas,
|
this.hass.areas,
|
||||||
this.hass.devices,
|
this.hass.devices,
|
||||||
this.hass.entities,
|
this.hass.entities,
|
||||||
this._floors!
|
this.hass.floors
|
||||||
);
|
);
|
||||||
let area: AreaRegistryEntry;
|
let area: AreaRegistryEntry;
|
||||||
if (ev.detail.oldPath === UNASSIGNED_PATH) {
|
if (ev.detail.oldPath === UNASSIGNED_PATH) {
|
||||||
|
@@ -86,7 +86,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
|||||||
this._unsubMql = undefined;
|
this._unsubMql = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig(): ChooseAction {
|
||||||
return { choose: [{ conditions: [], sequence: [] }] };
|
return { choose: [{ conditions: [], sequence: [] }] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public action!: Condition;
|
@property({ attribute: false }) public action!: Condition;
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
|
||||||
return { condition: "state" };
|
return { condition: "state" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,13 +87,12 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
const elClass = customElements.get(
|
const elClass = customElements.get(
|
||||||
`ha-automation-condition-${type}`
|
`ha-automation-condition-${type}`
|
||||||
) as CustomElementConstructor & {
|
) as CustomElementConstructor & {
|
||||||
defaultConfig: Omit<Condition, "condition">;
|
defaultConfig: Condition;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type !== this.action.condition) {
|
if (type !== this.action.condition) {
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: {
|
value: {
|
||||||
condition: type,
|
|
||||||
...elClass.defaultConfig,
|
...elClass.defaultConfig,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -19,7 +19,7 @@ export class HaDelayAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
@state() private _timeData?: HaDurationData;
|
@state() private _timeData?: HaDurationData;
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig(): DelayAction {
|
||||||
return { delay: "" };
|
return { delay: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -36,7 +36,7 @@ export class HaDeviceAction extends LitElement {
|
|||||||
|
|
||||||
private _origAction?: DeviceAction;
|
private _origAction?: DeviceAction;
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig(): DeviceAction {
|
||||||
return {
|
return {
|
||||||
device_id: "",
|
device_id: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
|
@@ -21,7 +21,7 @@ export class HaIfAction extends LitElement implements ActionElement {
|
|||||||
|
|
||||||
@state() private _showElse = false;
|
@state() private _showElse = false;
|
||||||
|
|
||||||
public static get defaultConfig() {
|
public static get defaultConfig(): IfAction {
|
||||||
return {
|
return {
|
||||||
if: [],
|
if: [],
|
||||||
then: [],
|
then: [],
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user