mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-20 21:01:30 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf728d9b4f | ||
|
|
0b79d23a53 | ||
|
|
f05f11bf47 | ||
|
|
1be2a691af |
No files matched your search
@@ -380,14 +380,6 @@ const createRspackConfig = ({
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
// @formatjs/bigdecimal uses BigInt (Safari 14, Chrome 67), which
|
||||
// breaks the NumberFormat and PluralRules polyfills
|
||||
...(!latestBuild && {
|
||||
"@formatjs/bigdecimal$": path.join(
|
||||
paths.root_dir,
|
||||
"src/resources/polyfills/bigdecimal.ts"
|
||||
),
|
||||
}),
|
||||
"lit/static-html$": "lit/static-html.js",
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
|
||||
+11
-6
@@ -63,7 +63,13 @@
|
||||
"@formatjs/intl-numberformat": "9.4.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.15",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.15",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.1",
|
||||
"@fullcalendar/core": "6.1.22",
|
||||
"@fullcalendar/daygrid": "6.1.22",
|
||||
"@fullcalendar/interaction": "6.1.22",
|
||||
"@fullcalendar/list": "6.1.22",
|
||||
"@fullcalendar/luxon3": "6.1.22",
|
||||
"@fullcalendar/timegrid": "6.1.22",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.0",
|
||||
"@lezer/common": "1.5.3",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
@@ -80,7 +86,7 @@
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@tsparticles/engine": "4.4.0",
|
||||
"@tsparticles/preset-links": "4.4.0",
|
||||
"@versatiles/style": "7.0.1",
|
||||
"@versatiles/style": "6.4.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.201.1",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -92,13 +98,11 @@
|
||||
"cropperjs": "1.7.0",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.4.0",
|
||||
"decimal.js": "10.6.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"echarts": "6.1.0",
|
||||
"echarts-extension-chart2music": "0.1.1",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.7.1",
|
||||
@@ -109,6 +113,7 @@
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"luxon": "3.7.2",
|
||||
"maplibre-gl": "6.14.0",
|
||||
"marked": "18.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -123,7 +128,6 @@
|
||||
"sortablejs": "1.15.7",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"temporal-polyfill": "1.0.6",
|
||||
"tinykeys": "4.0.1",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
@@ -164,6 +168,7 @@
|
||||
"@types/leaflet": "1.9.22",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.6",
|
||||
"@types/node": "24.19.2",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
@@ -176,7 +181,7 @@
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"core-js-compat": "3.50.0",
|
||||
"del": "9.0.1",
|
||||
"eslint": "10.13.0",
|
||||
"eslint": "10.12.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
|
||||
Generated
+152
-127
@@ -167,6 +167,7 @@ overrides:
|
||||
lit-html: 3.3.3
|
||||
clean-css: 5.3.3
|
||||
'@lit/reactive-element': 2.1.2
|
||||
'@fullcalendar/daygrid': 6.1.22
|
||||
tslib: 2.8.1
|
||||
|
||||
patchedDependencies:
|
||||
@@ -249,9 +250,27 @@ importers:
|
||||
'@formatjs/intl-relativetimeformat':
|
||||
specifier: 12.3.15
|
||||
version: 12.3.15
|
||||
'@fullcalendar/core':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22
|
||||
'@fullcalendar/daygrid':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22(@fullcalendar/[email protected])
|
||||
'@fullcalendar/interaction':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22(@fullcalendar/[email protected])
|
||||
'@fullcalendar/list':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22(@fullcalendar/[email protected])
|
||||
'@fullcalendar/luxon3':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22(@fullcalendar/[email protected])([email protected])
|
||||
'@fullcalendar/timegrid':
|
||||
specifier: 6.1.22
|
||||
version: 6.1.22(@fullcalendar/[email protected])
|
||||
'@home-assistant/webawesome':
|
||||
specifier: 3.14.0-ha.1
|
||||
version: 3.14.0-ha.1(@floating-ui/[email protected])(@types/[email protected])
|
||||
specifier: 3.14.0-ha.0
|
||||
version: 3.14.0-ha.0(@floating-ui/[email protected])(@types/[email protected])
|
||||
'@lezer/common':
|
||||
specifier: 1.5.3
|
||||
version: 1.5.3
|
||||
@@ -301,8 +320,8 @@ importers:
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
'@versatiles/style':
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1([email protected])
|
||||
specifier: 6.4.1
|
||||
version: 6.4.1([email protected])
|
||||
'@vibrant/color':
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4
|
||||
@@ -336,9 +355,6 @@ importers:
|
||||
date-fns:
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
decimal.js:
|
||||
specifier: 10.6.0
|
||||
version: 10.6.0
|
||||
deep-clone-simple:
|
||||
specifier: 1.1.1
|
||||
version: 1.1.1
|
||||
@@ -354,9 +370,6 @@ importers:
|
||||
element-internals-polyfill:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
fullcalendar:
|
||||
specifier: 7.1.3
|
||||
version: 7.1.3([email protected])
|
||||
fuse.js:
|
||||
specifier: 7.5.0
|
||||
version: 7.5.0
|
||||
@@ -387,6 +400,9 @@ importers:
|
||||
lit-html:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
luxon:
|
||||
specifier: 3.7.2
|
||||
version: 3.7.2
|
||||
maplibre-gl:
|
||||
specifier: 6.14.0
|
||||
version: 6.14.0
|
||||
@@ -429,9 +445,6 @@ importers:
|
||||
superstruct:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2
|
||||
temporal-polyfill:
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6
|
||||
tinykeys:
|
||||
specifier: 4.0.1
|
||||
version: 4.0.1(patch_hash=cdc7ece96d53357d38ff3c124a2553f86e3a5dc333d641d223eb6f07e28275f0)
|
||||
@@ -489,13 +502,13 @@ importers:
|
||||
version: 4.22.3([email protected])
|
||||
'@eslint/js':
|
||||
specifier: 10.0.1
|
||||
version: 10.0.1([email protected]3.0([email protected]))
|
||||
version: 10.0.1([email protected]2.0([email protected]))
|
||||
'@gfx/zopfli':
|
||||
specifier: 1.0.15
|
||||
version: 1.0.15
|
||||
'@html-eslint/eslint-plugin':
|
||||
specifier: 0.67.0
|
||||
version: 0.67.0([email protected]3.0([email protected]))
|
||||
version: 0.67.0([email protected]2.0([email protected]))
|
||||
'@lokalise/node-api':
|
||||
specifier: 16.6.0
|
||||
version: 16.6.0
|
||||
@@ -547,6 +560,9 @@ importers:
|
||||
'@types/lodash.merge':
|
||||
specifier: 4.6.9
|
||||
version: 4.6.9
|
||||
'@types/luxon':
|
||||
specifier: 3.7.6
|
||||
version: 3.7.6
|
||||
'@types/node':
|
||||
specifier: 24.19.2
|
||||
version: 24.19.2
|
||||
@@ -584,29 +600,29 @@ importers:
|
||||
specifier: 9.0.1
|
||||
version: 9.0.1
|
||||
eslint:
|
||||
specifier: 10.13.0
|
||||
version: 10.13.0([email protected])
|
||||
specifier: 10.12.0
|
||||
version: 10.12.0([email protected])
|
||||
eslint-config-prettier:
|
||||
specifier: 10.1.8
|
||||
version: 10.1.8([email protected]3.0([email protected]))
|
||||
version: 10.1.8([email protected]2.0([email protected]))
|
||||
eslint-import-resolver-webpack:
|
||||
specifier: 0.13.11
|
||||
version: 0.13.11([email protected])
|
||||
eslint-plugin-import-x:
|
||||
specifier: 4.17.1
|
||||
version: 4.17.1(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])
|
||||
version: 4.17.1(@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])
|
||||
eslint-plugin-lit:
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1([email protected]3.0([email protected]))
|
||||
version: 2.3.1([email protected]2.0([email protected]))
|
||||
eslint-plugin-lit-a11y:
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3([email protected]3.0([email protected]))
|
||||
version: 5.1.3([email protected]2.0([email protected]))
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: 4.4.1
|
||||
version: 4.4.1(@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))
|
||||
version: 4.4.1(@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))
|
||||
eslint-plugin-wc:
|
||||
specifier: 3.1.0
|
||||
version: 3.1.0([email protected]3.0([email protected]))
|
||||
version: 3.1.0([email protected]2.0([email protected]))
|
||||
fancy-log:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
@@ -696,7 +712,7 @@ importers:
|
||||
version: 6.0.3
|
||||
typescript-eslint:
|
||||
specifier: 8.71.2
|
||||
version: 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
version: 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
vite:
|
||||
specifier: 8.3.4
|
||||
version: 8.3.4(@types/[email protected])([email protected])([email protected])
|
||||
@@ -1994,16 +2010,34 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@full-ui/headless-calendar@7.1.3':
|
||||
resolution: {integrity: sha512-CFomIp87osqY65Hlu2c0e2ZnofBe9tKOO8IoPzIfSPVDK/+aGz4Itj6mLu4xPjIvzRDQiopcVzYlip6u1bvYxw==}
|
||||
peerDependencies:
|
||||
temporal-polyfill: ^1.0.1
|
||||
'@fullcalendar/core@6.1.22':
|
||||
resolution: {integrity: sha512-ju5PJUE++zr8XXCGtlXlrodF6doUy9BDfzx+VmE0B+XDBM2A5cUi/aU3M1ANfw/StmKhwqew7Ps7bXMiv/JREg==}
|
||||
|
||||
'@fullcalendar/core@7.1.3':
|
||||
resolution: {integrity: sha512-DLGXqX7CnuUQHH679FgE6+jc927wa708ydIcnfVx372IOK/OiDZjqc+IBwuKnu2EyqleCooDoAnUAcvXBhHosA==}
|
||||
'@fullcalendar/daygrid@6.1.22':
|
||||
resolution: {integrity: sha512-cpom5+bAvsGG5nX+snsCtpMIyyhAr+QlseD9mhz5Vq7rMBTzx43mSkz6IBvQ3vz5h2idbDmT1cggEn/dzZwt9Q==}
|
||||
peerDependencies:
|
||||
'@full-ui/headless-calendar': 7.1.3
|
||||
temporal-polyfill: ^1.0.1
|
||||
'@fullcalendar/core': ~6.1.22
|
||||
|
||||
'@fullcalendar/[email protected]':
|
||||
resolution: {integrity: sha512-r8Njw8aPNFGRwDjVAh9NTh3ZJib5tcUoLi/BM4uYJt5Cs9tDiirmImaqxhx56hclVFX7ktd5CdaMX0+ArSbm5A==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.22
|
||||
|
||||
'@fullcalendar/[email protected]':
|
||||
resolution: {integrity: sha512-7e5kZbeT3fhtXsts6VMQxbGtpoCFOZCxrIckHfn5VTrRh8Up0TKoYj/AOaIj94uz0qebWgaLXwd9Ijiu3uTwjg==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.22
|
||||
|
||||
'@fullcalendar/[email protected]':
|
||||
resolution: {integrity: sha512-or43T5SQAzb3d8U6Th6Q3vclas8rGEYSsJsUohC+jySJyJuqCLcqgW+NnbrSYIYXx3MUUk32h8au+BEjfjuo1A==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.22
|
||||
luxon: ^3.0.0
|
||||
|
||||
'@fullcalendar/[email protected]':
|
||||
resolution: {integrity: sha512-gNMtkXrkfN3iszXty+pYbJwGxcozL0xBVKYmEd1lm0DMEbrffpVJyV+vifmtl/eIbWCNRWrMWUtGLbYy/ukykA==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.22
|
||||
|
||||
'@gar/[email protected]':
|
||||
resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==}
|
||||
@@ -2021,8 +2055,8 @@ packages:
|
||||
resolution: {integrity: sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
'@home-assistant/[email protected].1':
|
||||
resolution: {integrity: sha512-h/HVT3LnDIk9rQ2+xAm0WTXFG89sV1KEAqLwjxl5tYBDAftQZCxV+5adchRref8anvBtp1opctjxHtuNBwgQ2g==}
|
||||
'@home-assistant/[email protected].0':
|
||||
resolution: {integrity: sha512-wG0a6AfDsYcUsT16bkvE8JHbsLTfUNNYoLEWI/ApRr4kewObcYwKhunFT+naLVYy8K9/Kbq6+O5nz57hu85ZWg==}
|
||||
engines: {node: '>=14.17.0'}
|
||||
|
||||
'@html-eslint/[email protected]':
|
||||
@@ -3332,6 +3366,9 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-6KSjliQAXK8ZLgFQO4B7iE4Rpf/B3rItzCFuXezBY/XIAGxOdLd7vRNK9/StRPwiS/yJsVbB7HKpW46B8tcEuQ==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
|
||||
|
||||
@@ -3668,11 +3705,11 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@versatiles/style@7.0.1':
|
||||
resolution: {integrity: sha512-ArZ09BwFWthylcvChgm8rwQ2FhIvvL4rRAOy1z+qn2tniOyEkEItBeQIJRTJlh09Wn8HsRgcMw9hFIfDLgWhSw==}
|
||||
'@versatiles/style@6.4.1':
|
||||
resolution: {integrity: sha512-0FpDO6PTEUBeWkvkSRt6l9N+EC9+rK8wuGgsxrzjVAMTSCoWG/t4zYjRzYYDkjgVpPXKB02s3/WbuGJiYUw8AA==}
|
||||
engines: {node: '>=20'}
|
||||
peerDependencies:
|
||||
maplibre-gl: '>=5.5.0'
|
||||
maplibre-gl: '>=5.0.0'
|
||||
peerDependenciesMeta:
|
||||
maplibre-gl:
|
||||
optional: true
|
||||
@@ -4792,8 +4829,8 @@ packages:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
[email protected]3.0:
|
||||
resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==}
|
||||
[email protected]2.0:
|
||||
resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5010,11 +5047,6 @@ packages:
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Mn8VNqLX80C93UFh0s9aKqhHUSh0BwK8pczyQ+nNDKutJeM0/SVLkJoizTfX9sdmwGiS6+hkAHAg9YC10Us0gg==}
|
||||
peerDependencies:
|
||||
temporal-polyfill: ^1.0.1
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
@@ -5866,6 +5898,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -6440,13 +6476,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
|
||||
|
||||
preact@10.29.8:
|
||||
resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==}
|
||||
peerDependencies:
|
||||
preact-render-to-string: '>=5'
|
||||
peerDependenciesMeta:
|
||||
preact-render-to-string:
|
||||
optional: true
|
||||
preact@10.12.1:
|
||||
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
@@ -7074,15 +7105,6 @@ packages:
|
||||
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-tYyEry1HLGUDbm2356W3iYD0OZdo7rlP7h98Zr/EnjqLZQwTAfcBDUvJnJbfA15v/vQad57hJZScG5O0W2oKEA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-wxVoanmDeavXie1vu2JaQ3WIc3JZnWAOYFBsJyATaVsXsycKYUflGsyBmrRSnoCpZJpwPyr38VpgSUlQ8CbFxg==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-9jbTSX3HvkOWvDpNpFzwX/d1mkCrgQgwLmVTOEiuocZGKHsrQJtttGZFmybJ2zYP5zCSZuU5ZyeS8+eOEPABiQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -9306,9 +9328,9 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@eslint-community/[email protected]([email protected]3.0([email protected]))':
|
||||
'@eslint-community/[email protected]([email protected]2.0([email protected]))':
|
||||
dependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/[email protected]': {}
|
||||
@@ -9333,9 +9355,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/[email protected]([email protected]3.0([email protected]))':
|
||||
'@eslint/[email protected]([email protected]2.0([email protected]))':
|
||||
optionalDependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
|
||||
'@eslint/[email protected]': {}
|
||||
|
||||
@@ -9455,14 +9477,31 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@full-ui/[email protected]([email protected])':
|
||||
'@fullcalendar/[email protected]':
|
||||
dependencies:
|
||||
temporal-polyfill: 1.0.6
|
||||
preact: 10.12.1
|
||||
|
||||
'@fullcalendar/core@7.1.3(@full-ui/[email protected]([email protected]))([email protected])':
|
||||
'@fullcalendar/daygrid@6.1.22(@fullcalendar/[email protected])':
|
||||
dependencies:
|
||||
'@full-ui/headless-calendar': 7.1.3([email protected])
|
||||
temporal-polyfill: 1.0.6
|
||||
'@fullcalendar/core': 6.1.22
|
||||
|
||||
'@fullcalendar/[email protected](@fullcalendar/[email protected])':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.22
|
||||
|
||||
'@fullcalendar/[email protected](@fullcalendar/[email protected])':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.22
|
||||
|
||||
'@fullcalendar/[email protected](@fullcalendar/[email protected])([email protected])':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.22
|
||||
luxon: 3.7.2
|
||||
|
||||
'@fullcalendar/[email protected](@fullcalendar/[email protected])':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.22
|
||||
'@fullcalendar/daygrid': 6.1.22(@fullcalendar/[email protected])
|
||||
|
||||
'@gar/[email protected]': {}
|
||||
|
||||
@@ -9476,7 +9515,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-negated-glob: 1.0.0
|
||||
|
||||
'@home-assistant/[email protected].1(@floating-ui/[email protected])(@types/[email protected])':
|
||||
'@home-assistant/[email protected].0(@floating-ui/[email protected])(@types/[email protected])':
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.1.0
|
||||
'@floating-ui/dom': 1.8.0
|
||||
@@ -9501,7 +9540,7 @@ snapshots:
|
||||
'@html-eslint/types': 0.67.0
|
||||
html-standard: 0.0.13
|
||||
|
||||
'@html-eslint/[email protected]([email protected]3.0([email protected]))':
|
||||
'@html-eslint/[email protected]([email protected]2.0([email protected]))':
|
||||
dependencies:
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@html-eslint/core': 0.67.0
|
||||
@@ -9510,7 +9549,7 @@ snapshots:
|
||||
'@html-eslint/template-syntax-parser': 0.67.0
|
||||
'@html-eslint/types': 0.67.0
|
||||
'@rviscomi/capo.js': 2.2.1
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
html-standard: 0.0.13
|
||||
|
||||
'@html-eslint/[email protected]':
|
||||
@@ -11013,6 +11052,8 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected]':
|
||||
@@ -11051,15 +11092,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.19.2
|
||||
|
||||
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/scope-manager': 8.71.2
|
||||
'@typescript-eslint/type-utils': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/type-utils': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/visitor-keys': 8.71.2
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
ignore: 7.0.6
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.5.0([email protected])
|
||||
@@ -11067,14 +11108,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.71.2
|
||||
'@typescript-eslint/types': 8.71.2
|
||||
'@typescript-eslint/typescript-estree': 8.71.2([email protected])([email protected])
|
||||
'@typescript-eslint/visitor-keys': 8.71.2
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11097,13 +11138,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.71.2
|
||||
'@typescript-eslint/typescript-estree': 8.71.2([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
ts-api-utils: 2.5.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -11126,13 +11167,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]3.0([email protected]))
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]2.0([email protected]))
|
||||
'@typescript-eslint/scope-manager': 8.71.2
|
||||
'@typescript-eslint/types': 8.71.2
|
||||
'@typescript-eslint/typescript-estree': 8.71.2([email protected])([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11272,7 +11313,7 @@ snapshots:
|
||||
'@unrs/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@versatiles/style@7.0.1([email protected])':
|
||||
'@versatiles/style@6.4.1([email protected])':
|
||||
dependencies:
|
||||
'@maplibre/maplibre-gl-style-spec': 26.4.4
|
||||
optionalDependencies:
|
||||
@@ -12368,9 +12409,9 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
[email protected]([email protected]2.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
@@ -12394,12 +12435,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected]):
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.71.2
|
||||
comment-parser: 1.4.8
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
eslint-import-context: 0.1.9([email protected])
|
||||
is-glob: 4.0.3
|
||||
minimatch: 10.2.6
|
||||
@@ -12407,39 +12448,39 @@ snapshots:
|
||||
stable-hash-x: 0.2.0
|
||||
unrs-resolver: 1.12.2
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
[email protected]([email protected]2.0([email protected])):
|
||||
dependencies:
|
||||
'@thepassle/axobject-query': 4.0.0
|
||||
aria-query: 5.3.2
|
||||
axe-core: 4.13.0
|
||||
dom5: 3.0.1
|
||||
emoji-regex: 10.6.0
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint-plugin-lit: 2.3.1([email protected]3.0([email protected]))
|
||||
eslint: 10.12.0([email protected])
|
||||
eslint-plugin-lit: 2.3.1([email protected]2.0([email protected]))
|
||||
eslint-rule-extender: 0.0.1
|
||||
language-tags: 1.0.9
|
||||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 6.0.1
|
||||
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
[email protected]([email protected]2.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
parse5: 8.0.1
|
||||
parse5-htmlparser2-tree-adapter: 8.0.1
|
||||
|
||||
[email protected](@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected])):
|
||||
[email protected](@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
[email protected]([email protected]2.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
is-valid-element-name: 1.0.0
|
||||
js-levenshtein-esm: 2.0.0
|
||||
|
||||
@@ -12456,9 +12497,9 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]3.0([email protected]):
|
||||
[email protected]2.0([email protected]):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]3.0([email protected]))
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]2.0([email protected]))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.23.5([email protected])
|
||||
'@eslint/config-helpers': 0.7.0
|
||||
@@ -12710,15 +12751,6 @@ snapshots:
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
'@full-ui/headless-calendar': 7.1.3([email protected])
|
||||
'@fullcalendar/core': 7.1.3(@full-ui/[email protected]([email protected]))([email protected])
|
||||
preact: 10.29.8
|
||||
temporal-polyfill: 1.0.6
|
||||
transitivePeerDependencies:
|
||||
- preact-render-to-string
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -13606,6 +13638,8 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.6.0
|
||||
@@ -14262,7 +14296,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
preact@10.29.8: {}
|
||||
preact@10.12.1: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -15017,15 +15051,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
temporal-spec: 1.0.1
|
||||
temporal-utils: 1.0.3
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-stream: 2.0.1
|
||||
@@ -15203,13 +15228,13 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
[email protected]([email protected]3.0([email protected]))([email protected])([email protected]):
|
||||
[email protected]([email protected]2.0([email protected]))([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/eslint-plugin': 8.71.2(@typescript-eslint/[email protected]([email protected]2.0([email protected]))([email protected])([email protected]))([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/typescript-estree': 8.71.2([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
'@typescript-eslint/utils': 8.71.2([email protected]2.0([email protected]))([email protected])([email protected])
|
||||
eslint: 10.12.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -26,6 +26,7 @@ overrides:
|
||||
lit-html: 3.3.3
|
||||
clean-css: 5.3.3
|
||||
"@lit/reactive-element": 2.1.2
|
||||
"@fullcalendar/daygrid": 6.1.22
|
||||
tslib: 2.8.1
|
||||
|
||||
patchedDependencies:
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
import { replaceCurrentUrl, type NavigateOptions } from "../navigate";
|
||||
import { replaceCurrentUrl } from "../navigate";
|
||||
import { constructUrlCurrentPath } from "../url/construct-url";
|
||||
import { currentPath } from "../url/current-path";
|
||||
import {
|
||||
@@ -30,7 +30,7 @@ export interface QueryParamsControllerOptions {
|
||||
* shared, bookmarked and reloaded. Writes replace the current history entry,
|
||||
* so Back leaves the page instead of undoing each change, and only touch the
|
||||
* config's params. The controller only acts while the URL is still the
|
||||
* page the host was connected on, or the default page its router opened.
|
||||
* page the host was connected on.
|
||||
*/
|
||||
export class QueryParamsController<
|
||||
C extends QueryParamConfig,
|
||||
@@ -120,16 +120,8 @@ export class QueryParamsController<
|
||||
return this._options.enabled() && currentPath() === this._path;
|
||||
}
|
||||
|
||||
private _navigated = (ev?: Event & { detail?: NavigateOptions }) => {
|
||||
private _navigated = () => {
|
||||
this._cancelPending();
|
||||
// A router opening its default page, like /config/devices going to
|
||||
// /config/devices/dashboard, replaces the URL after the host connected
|
||||
const path = currentPath();
|
||||
|
||||
if (ev?.detail?.replace && path.startsWith(`${this._path}/`)) {
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
// Leaving the page gets here too, before the router swaps the page
|
||||
if (this._isActive()) {
|
||||
this._options.navigated();
|
||||
|
||||
@@ -45,168 +45,69 @@ export const formatNumericDuration = (
|
||||
return null;
|
||||
};
|
||||
|
||||
const DURATION_FIELDS = [
|
||||
["days", "day"],
|
||||
["hours", "hour"],
|
||||
["minutes", "minute"],
|
||||
["seconds", "second"],
|
||||
["milliseconds", "millisecond"],
|
||||
] as const satisfies readonly (readonly [keyof HaDurationData, string])[];
|
||||
|
||||
type DurationFormatOptions = {
|
||||
style: "long" | "narrow" | "digital";
|
||||
} & Partial<Record<`${keyof HaDurationData}Display`, "always" | "auto">>;
|
||||
|
||||
type DurationFormatter = (duration: HaDurationData) => string;
|
||||
|
||||
// Throws like Intl.DurationFormat on fractions, NaN and mixed signs
|
||||
const isNegativeDuration = (duration: HaDurationData) => {
|
||||
const values = DURATION_FIELDS.map(([field]) => Number(duration[field] ?? 0));
|
||||
const negative = values.some((value) => value < 0);
|
||||
if (
|
||||
!values.every((value) => Number.isInteger(value)) ||
|
||||
(negative && values.some((value) => value > 0))
|
||||
) {
|
||||
throw new RangeError("Invalid duration");
|
||||
}
|
||||
return negative;
|
||||
};
|
||||
|
||||
const fallbackUnitsFormat = (
|
||||
language: string,
|
||||
options: DurationFormatOptions
|
||||
): DurationFormatter => {
|
||||
const style = options.style === "narrow" ? "narrow" : "long";
|
||||
const list = new Intl.ListFormat(language, { type: "unit", style });
|
||||
const unitFormats = DURATION_FIELDS.map(
|
||||
([, unit]) =>
|
||||
new Intl.NumberFormat(language, {
|
||||
style: "unit",
|
||||
unit,
|
||||
unitDisplay: style,
|
||||
})
|
||||
);
|
||||
return (duration) => {
|
||||
const negative = isNegativeDuration(duration);
|
||||
const parts: string[] = [];
|
||||
DURATION_FIELDS.forEach(([field], index) => {
|
||||
const value = Math.abs(duration[field] ?? 0);
|
||||
if (value || options[`${field}Display`] === "always") {
|
||||
// Like Intl.DurationFormat, only the first unit shows the sign
|
||||
parts.push(
|
||||
unitFormats[index].format(negative && !parts.length ? -value : value)
|
||||
);
|
||||
}
|
||||
});
|
||||
return list.format(parts);
|
||||
};
|
||||
};
|
||||
|
||||
const fallbackDigitalFormat = (
|
||||
language: string,
|
||||
options: DurationFormatOptions
|
||||
): DurationFormatter => {
|
||||
const list = new Intl.ListFormat(language, { type: "unit", style: "short" });
|
||||
const dayFormat = new Intl.NumberFormat(language, {
|
||||
style: "unit",
|
||||
unit: "day",
|
||||
unitDisplay: "short",
|
||||
});
|
||||
const hourFormat = new Intl.NumberFormat(language, { useGrouping: false });
|
||||
const twoDigitFormat = new Intl.NumberFormat(language, {
|
||||
minimumIntegerDigits: 2,
|
||||
maximumFractionDigits: 3,
|
||||
useGrouping: false,
|
||||
});
|
||||
return (duration) => {
|
||||
const sign = isNegativeDuration(duration) ? "-" : "";
|
||||
const days = Math.abs(duration.days ?? 0);
|
||||
const hours = Math.abs(duration.hours ?? 0);
|
||||
const minutes = Math.abs(duration.minutes ?? 0);
|
||||
const seconds =
|
||||
Math.abs(duration.seconds ?? 0) +
|
||||
Math.abs(duration.milliseconds ?? 0) / 1000;
|
||||
let time = `${twoDigitFormat.format(minutes)}:${twoDigitFormat.format(seconds)}`;
|
||||
if (hours || options.hoursDisplay !== "auto") {
|
||||
time = `${hourFormat.format(hours)}:${time}`;
|
||||
}
|
||||
return days
|
||||
? list.format([sign + dayFormat.format(days), time])
|
||||
: sign + time;
|
||||
};
|
||||
};
|
||||
|
||||
// The Intl.DurationFormat polyfill needs BigInt, which some legacy browsers lack
|
||||
const createDurationFormat = (
|
||||
language: string,
|
||||
options: DurationFormatOptions
|
||||
): DurationFormatter => {
|
||||
if ("DurationFormat" in Intl) {
|
||||
const durationFormat = new Intl.DurationFormat(language, options);
|
||||
return (duration) => durationFormat.format(duration);
|
||||
}
|
||||
return options.style === "digital"
|
||||
? fallbackDigitalFormat(language, options)
|
||||
: fallbackUnitsFormat(language, options);
|
||||
};
|
||||
|
||||
const formatDurationLongMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "long",
|
||||
})
|
||||
const formatDurationLongMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "long",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDurationLong = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => formatDurationLongMem(locale)(duration);
|
||||
) => formatDurationLongMem(locale).format(duration);
|
||||
|
||||
const formatDurationNarrowMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
})
|
||||
const formatDurationNarrowMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDurationNarrow = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => formatDurationNarrowMem(locale)(duration);
|
||||
) => formatDurationNarrowMem(locale).format(duration);
|
||||
|
||||
const formatDigitalDurationMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "digital",
|
||||
hoursDisplay: "auto",
|
||||
})
|
||||
const formatDigitalDurationMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "digital",
|
||||
hoursDisplay: "auto",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDurationDigital = (
|
||||
locale: FrontendLocaleData,
|
||||
duration: HaDurationData
|
||||
) => formatDigitalDurationMem(locale)(duration);
|
||||
) => formatDigitalDurationMem(locale).format(duration);
|
||||
|
||||
export const DURATION_UNITS = ["min", "h", "d"] as const;
|
||||
|
||||
type DurationUnit = (typeof DURATION_UNITS)[number];
|
||||
|
||||
const formatDurationDayMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
daysDisplay: "always",
|
||||
})
|
||||
const formatDurationDayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
daysDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
const formatDurationHourMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
hoursDisplay: "always",
|
||||
})
|
||||
const formatDurationHourMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
hoursDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
const formatDurationMinuteMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||
createDurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
minutesDisplay: "always",
|
||||
})
|
||||
const formatDurationMinuteMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DurationFormat(locale.language, {
|
||||
style: "narrow",
|
||||
minutesDisplay: "always",
|
||||
})
|
||||
);
|
||||
|
||||
export const formatDuration = (
|
||||
@@ -228,7 +129,7 @@ export const formatDuration = (
|
||||
days,
|
||||
hours,
|
||||
};
|
||||
return formatDurationDayMem(locale)(input);
|
||||
return formatDurationDayMem(locale).format(input);
|
||||
}
|
||||
case "h": {
|
||||
const hours = Math.floor(value);
|
||||
@@ -237,7 +138,7 @@ export const formatDuration = (
|
||||
hours,
|
||||
minutes,
|
||||
};
|
||||
return formatDurationHourMem(locale)(input);
|
||||
return formatDurationHourMem(locale).format(input);
|
||||
}
|
||||
case "min": {
|
||||
const minutes = Math.floor(value);
|
||||
@@ -246,7 +147,7 @@ export const formatDuration = (
|
||||
minutes,
|
||||
seconds,
|
||||
};
|
||||
return formatDurationMinuteMem(locale)(input);
|
||||
return formatDurationMinuteMem(locale).format(input);
|
||||
}
|
||||
default:
|
||||
throw new Error("Invalid duration unit");
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type {
|
||||
DataTableFilter,
|
||||
DataTableFilters,
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
// The filters of the devices table. Search, sorting and grouping are kept by
|
||||
// the data table, see data-table-query-params.
|
||||
export const devicesFilterQueryParamConfig = {
|
||||
list: [
|
||||
"status",
|
||||
"floor",
|
||||
"area",
|
||||
"domain",
|
||||
"label",
|
||||
"config_entry",
|
||||
"sub_entry",
|
||||
],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type DevicesFilterQueryParams = QueryParamValues<
|
||||
typeof devicesFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
const list = (filter: DataTableFilter | undefined): string[] | undefined =>
|
||||
Array.isArray(filter?.value) ? filter.value : undefined;
|
||||
|
||||
const floorAreas = (
|
||||
filter: DataTableFilter | undefined
|
||||
): Record<string, string[]> | undefined =>
|
||||
filter?.value && !Array.isArray(filter.value) ? filter.value : undefined;
|
||||
|
||||
export const devicesFilterQueryParams = (
|
||||
filters: DataTableFilters
|
||||
): DevicesFilterQueryParams => ({
|
||||
status: list(filters["ha-filter-states"]),
|
||||
floor: floorAreas(filters["ha-filter-floor-areas"])?.floors,
|
||||
area: floorAreas(filters["ha-filter-floor-areas"])?.areas,
|
||||
domain: list(filters["ha-filter-integrations"]),
|
||||
label: list(filters["ha-filter-labels"]),
|
||||
config_entry: list(filters.config_entry),
|
||||
sub_entry: list(filters.sub_entry),
|
||||
});
|
||||
|
||||
// What a link says to show, undefined when it has no table params so what was
|
||||
// picked before stays. A link replaces the search and the filters, ones left
|
||||
// behind would hide part of what it shows. The table applies its sorting and
|
||||
// grouping. The filters work out their items from the values.
|
||||
export interface DevicesLink {
|
||||
search: string;
|
||||
filters: DataTableFilters;
|
||||
}
|
||||
|
||||
export const devicesLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): DevicesLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, devicesFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(searchParams, devicesFilterQueryParamConfig);
|
||||
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
const values = (key: keyof DevicesFilterQueryParams): string[] =>
|
||||
params[key]?.filter(Boolean) ?? [];
|
||||
|
||||
const filter = (value: DataTableFilter["value"]): DataTableFilter => ({
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
|
||||
const floors = values("floor");
|
||||
const areas = values("area");
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
"ha-filter-states": filter(values("status")),
|
||||
"ha-filter-floor-areas": filter(
|
||||
floors.length || areas.length ? { floors, areas } : undefined
|
||||
),
|
||||
"ha-filter-integrations": filter(values("domain")),
|
||||
"ha-filter-labels": filter(values("label")),
|
||||
config_entry: filter(values("config_entry")),
|
||||
sub_entry: filter(values("sub_entry")),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1504,10 +1504,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table {
|
||||
background: var(
|
||||
--ha-data-table-background,
|
||||
var(--data-table-background-color)
|
||||
);
|
||||
background-color: var(--data-table-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
@@ -1779,6 +1776,7 @@ export class HaDataTable extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
@@ -1929,10 +1927,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.mdc-data-table__row:has(.group-header) {
|
||||
background: var(
|
||||
--ha-data-table-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.mdc-data-table__table.auto-height .scroller {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import WaCheckbox from "@home-assistant/webawesome/dist/components/checkbox/checkbox";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant checkbox component
|
||||
@@ -40,13 +40,9 @@ import { customElement, property } from "lit/decorators";
|
||||
* @attr {boolean} disabled - Disables the checkbox.
|
||||
* @attr {boolean} indeterminate - Draws the checkbox in an indeterminate state.
|
||||
* @attr {boolean} required - Makes the checkbox a required field.
|
||||
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the checked color variant. "brand" is default.
|
||||
*/
|
||||
@customElement("ha-checkbox")
|
||||
export class HaCheckbox extends WaCheckbox {
|
||||
@property({ reflect: true })
|
||||
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
||||
|
||||
/**
|
||||
* Returns the configured checkbox value, independent of checked state.
|
||||
*
|
||||
@@ -82,19 +78,15 @@ export class HaCheckbox extends WaCheckbox {
|
||||
);
|
||||
--checked-icon-color: var(
|
||||
--ha-checkbox-checked-icon-color,
|
||||
var(--checkbox-on-fill-color)
|
||||
var(--wa-color-brand-on-loud)
|
||||
);
|
||||
|
||||
--wa-form-control-activated-color: var(
|
||||
--ha-checkbox-checked-background-color,
|
||||
var(--checkbox-fill-color)
|
||||
var(--ha-color-fill-primary-loud-resting)
|
||||
);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
--checkbox-fill-color: var(--ha-color-fill-primary-loud-resting);
|
||||
--checkbox-fill-color-hover: var(--ha-color-fill-primary-loud-hover);
|
||||
--checkbox-on-fill-color: var(--wa-color-brand-on-loud);
|
||||
|
||||
--checked-icon-scale: var(--ha-checkbox-checked-icon-scale, 0.9);
|
||||
--wa-form-control-required-content: var(
|
||||
--ha-checkbox-required-marker,
|
||||
@@ -106,30 +98,6 @@ export class HaCheckbox extends WaCheckbox {
|
||||
);
|
||||
}
|
||||
|
||||
:host([variant="neutral"]) {
|
||||
--checkbox-fill-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
--checkbox-fill-color-hover: var(--ha-color-fill-neutral-loud-hover);
|
||||
--checkbox-on-fill-color: var(--ha-color-on-neutral-loud);
|
||||
}
|
||||
|
||||
:host([variant="success"]) {
|
||||
--checkbox-fill-color: var(--ha-color-fill-success-loud-resting);
|
||||
--checkbox-fill-color-hover: var(--ha-color-fill-success-loud-hover);
|
||||
--checkbox-on-fill-color: var(--ha-color-on-success-loud);
|
||||
}
|
||||
|
||||
:host([variant="warning"]) {
|
||||
--checkbox-fill-color: var(--ha-color-fill-warning-loud-resting);
|
||||
--checkbox-fill-color-hover: var(--ha-color-fill-warning-loud-hover);
|
||||
--checkbox-on-fill-color: var(--ha-color-on-warning-loud);
|
||||
}
|
||||
|
||||
:host([variant="danger"]) {
|
||||
--checkbox-fill-color: var(--ha-color-fill-danger-loud-resting);
|
||||
--checkbox-fill-color-hover: var(--ha-color-fill-danger-loud-hover);
|
||||
--checkbox-on-fill-color: var(--ha-color-on-danger-loud);
|
||||
}
|
||||
|
||||
[part~="base"] {
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
@@ -190,11 +158,11 @@ export class HaCheckbox extends WaCheckbox {
|
||||
label:has(input:indeterminate:not(:disabled)):hover [part~="control"] {
|
||||
background-color: var(
|
||||
--ha-checkbox-checked-background-color-hover,
|
||||
var(--checkbox-fill-color-hover)
|
||||
var(--ha-color-fill-primary-loud-hover)
|
||||
);
|
||||
border-color: var(
|
||||
--ha-checkbox-checked-background-color-hover,
|
||||
var(--checkbox-fill-color-hover)
|
||||
var(--ha-color-fill-primary-loud-hover)
|
||||
);
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -346,8 +346,7 @@ export class HaControlSelect extends LitElement {
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
unicode-bidi: plaintext;
|
||||
text-overflow: ellipsis;
|
||||
hyphens: manual;
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import { customElement } from "lit/decorators";
|
||||
*
|
||||
* @remarks
|
||||
* **Button Styling Guidance:**
|
||||
* - `primaryAction` slot: Use the default `appearance="accent"`
|
||||
* - `secondaryAction` slot: Use `appearance="plain"`
|
||||
* - `primaryAction` slot: Use `variant="accent"`
|
||||
* - `secondaryAction` slot: Use `variant="plain"`
|
||||
*/
|
||||
@customElement("ha-dialog-footer")
|
||||
export class HaDialogFooter extends LitElement {
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import {
|
||||
mdiDotsHorizontal,
|
||||
mdiFilterVariantRemove,
|
||||
mdiPuzzle,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../common/controllers/filter-panel-controller";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import {
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../data/context";
|
||||
import type { LogbookCauseGroup } from "../data/logbook";
|
||||
import { LOGBOOK_CAUSE_GROUPS, logbookUserCauseKey } from "../data/logbook";
|
||||
import type { User } from "../data/user";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
interface PersonUser {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const GROUP_ICONS: Record<LogbookCauseGroup, string> = {
|
||||
automation: mdiRobot,
|
||||
script: mdiScriptText,
|
||||
integration: mdiPuzzle,
|
||||
other: mdiDotsHorizontal,
|
||||
};
|
||||
|
||||
@customElement("ha-filter-logbook-causes")
|
||||
export class HaFilterLogbookCauses extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@state()
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@state()
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@state()
|
||||
private _formatters!: ContextType<typeof formattersContext>;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@query("ha-list-selectable") private _list?: HaListSelectable;
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
protected render() {
|
||||
const count = this.value?.length ?? 0;
|
||||
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.components.logbook.cause_filter.caption")}
|
||||
${
|
||||
count
|
||||
? html`<div class="badge">${count}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
.label=${this._localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${this._panel.showContent ? this._renderContent() : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderContent() {
|
||||
const users = this._personUsers(
|
||||
this._states,
|
||||
this._formatters,
|
||||
this._i18n.locale.language
|
||||
);
|
||||
const value = this.value ?? [];
|
||||
|
||||
return html`<div class="content" ${ref(this._content)}>
|
||||
<ha-list-selectable
|
||||
multi
|
||||
controlled
|
||||
aria-label=${this._localize("ui.components.logbook.cause_filter.caption")}
|
||||
@ha-list-item-selected=${this._handleItemToggled}
|
||||
@ha-list-item-deselected=${this._handleItemToggled}
|
||||
>
|
||||
${repeat(
|
||||
users,
|
||||
(user) => user.id,
|
||||
(user) => {
|
||||
const key = logbookUserCauseKey(user.id);
|
||||
return html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${key}
|
||||
.selected=${value.includes(key)}
|
||||
>
|
||||
<ha-user-badge slot="start" .user=${user as User}></ha-user-badge>
|
||||
<span slot="headline">${user.name}</span>
|
||||
</ha-list-item-option>`;
|
||||
}
|
||||
)}
|
||||
${LOGBOOK_CAUSE_GROUPS.map(
|
||||
(group) =>
|
||||
html`<ha-list-item-option
|
||||
appearance="checkbox"
|
||||
selection-position="end"
|
||||
.value=${group}
|
||||
.selected=${value.includes(group)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${GROUP_ICONS[group]}
|
||||
></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this._localize(
|
||||
`ui.components.logbook.cause_filter.${group}`
|
||||
)}</span
|
||||
>
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _personUsers = memoizeOne(
|
||||
(
|
||||
states: ContextType<typeof statesContext>,
|
||||
formatters: ContextType<typeof formattersContext>,
|
||||
language: string | undefined
|
||||
): PersonUser[] =>
|
||||
Object.values(states)
|
||||
.filter(
|
||||
(stateObj) =>
|
||||
computeStateDomain(stateObj) === "person" &&
|
||||
stateObj.attributes.user_id
|
||||
)
|
||||
.map((stateObj) => ({
|
||||
id: stateObj.attributes.user_id as string,
|
||||
name: formatters.formatEntityName(stateObj),
|
||||
}))
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
);
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemToggled(ev: CustomEvent<number>) {
|
||||
const option = this._list?.items[ev.detail] as HaListItemOption | undefined;
|
||||
const key = option?.value;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const value = new Set(this.value ?? []);
|
||||
if (!value.delete(key)) {
|
||||
value.add(key);
|
||||
}
|
||||
this.value = [...value];
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value.length ? this.value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list-selectable {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
ha-list-selectable::part(base) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-user-badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-logbook-causes": HaFilterLogbookCauses;
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@ export class HaFilterPane extends LitElement {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
@@ -10,6 +10,8 @@ import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
|
||||
import { entityTypeFilterFunc } from "../data/entity/entity_type";
|
||||
import type { EntitySources } from "../data/entity/entity_sources";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-filter-entity-types";
|
||||
import "./ha-filter-integrations";
|
||||
import "./ha-target-picker";
|
||||
|
||||
/**
|
||||
@@ -37,16 +39,6 @@ export const countTargets = (target: HassServiceTarget): number =>
|
||||
0
|
||||
);
|
||||
|
||||
/** Sets one filter from the value of its `data-table-filter-changed` event. */
|
||||
export const setSourceFilter = <T extends SourceFilters>(
|
||||
filters: T,
|
||||
key: keyof T,
|
||||
value: DataTableFiltersValue
|
||||
): T => ({
|
||||
...filters,
|
||||
[key]: Array.isArray(value) && value.length ? value : undefined,
|
||||
});
|
||||
|
||||
/** Number of filters that have at least one option selected. */
|
||||
export const countSourceFilters = (filters: SourceFilters): number =>
|
||||
Object.values(filters).filter((value) => value?.length).length;
|
||||
@@ -100,8 +92,8 @@ export const applySourceFilters = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Picks the targets a page shows. Meant to be placed in an `ha-filter-pane`,
|
||||
* above the filters the page narrows them down with.
|
||||
* Picker for what a page shows: the targets to include, narrowed down by
|
||||
* entity type and integration. Meant to be placed in an `ha-filter-pane`.
|
||||
*
|
||||
* The pages resolve every entity of a target, secondary ones included, so the
|
||||
* target picker counts them too.
|
||||
@@ -112,7 +104,6 @@ export class HaSourcesPicker extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value: HassServiceTarget = {};
|
||||
|
||||
/** The filters the page applies, so the picker counts what they keep. */
|
||||
@property({ attribute: false }) public filters: SourceFilters = {};
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -125,6 +116,10 @@ export class HaSourcesPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
// Only one filter panel is expanded at a time, so that the expanded one can
|
||||
// use the height that is left in the pane.
|
||||
@state() private _expandedFilter?: keyof SourceFilters;
|
||||
|
||||
protected render() {
|
||||
const noTargets = countTargets(this.value) === 0;
|
||||
|
||||
@@ -149,16 +144,72 @@ export class HaSourcesPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetsChanged}
|
||||
></ha-target-picker>
|
||||
<div
|
||||
class=${classMap({ filters: true, expanded: !!this._expandedFilter })}
|
||||
>
|
||||
<ha-filter-entity-types
|
||||
.value=${this.filters.types}
|
||||
.expanded=${this._expandedFilter === "types"}
|
||||
@data-table-filter-changed=${this._typesChanged}
|
||||
@expanded-changed=${this._typesExpanded}
|
||||
></ha-filter-entity-types>
|
||||
<ha-filter-integrations
|
||||
.value=${this.filters.integrations}
|
||||
.expanded=${this._expandedFilter === "integrations"}
|
||||
@data-table-filter-changed=${this._integrationsChanged}
|
||||
@expanded-changed=${this._integrationsExpanded}
|
||||
></ha-filter-integrations>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _activeFilter = memoizeOne(sourceFilterFunc);
|
||||
|
||||
protected firstUpdated() {
|
||||
// The filter panels label themselves with keys from the config panel.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
|
||||
private _targetsChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value || {} });
|
||||
}
|
||||
|
||||
private _typesChanged(ev: CustomEvent) {
|
||||
this._filterChanged("types", ev);
|
||||
}
|
||||
|
||||
private _integrationsChanged(ev: CustomEvent) {
|
||||
this._filterChanged("integrations", ev);
|
||||
}
|
||||
|
||||
private _filterChanged(key: keyof SourceFilters, ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as DataTableFiltersValue;
|
||||
fireEvent(this, "source-filters-changed", {
|
||||
value: {
|
||||
...this.filters,
|
||||
[key]: Array.isArray(value) && value.length ? value : undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _typesExpanded(ev: CustomEvent) {
|
||||
this._filterExpanded("types", ev);
|
||||
}
|
||||
|
||||
private _integrationsExpanded(ev: CustomEvent) {
|
||||
this._filterExpanded("integrations", ev);
|
||||
}
|
||||
|
||||
private _filterExpanded(key: keyof SourceFilters, ev: CustomEvent) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = key;
|
||||
} else if (this._expandedFilter === key) {
|
||||
this._expandedFilter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
/* The sections are laid out by the pane, so that an expanded filter
|
||||
panel can use the height that is left. */
|
||||
@@ -190,28 +241,28 @@ export class HaSourcesPicker extends LitElement {
|
||||
ha-target-picker.no-padding-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
/* An expanded panel sizes itself to the space that is left over, but
|
||||
keeps room for its list when the targets fill the pane. */
|
||||
.filters.expanded {
|
||||
flex: 1 1 auto;
|
||||
min-height: min(360px, 100%);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Lays out the filters that follow the picker in the pane. */
|
||||
export const sourceFiltersStyles = css`
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 auto;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
/* An expanded panel sizes itself to the space that is left over, but
|
||||
keeps room for its list when the targets fill the pane. */
|
||||
.filters.expanded {
|
||||
flex: 1 1 auto;
|
||||
min-height: min(360px, 100%);
|
||||
}
|
||||
`;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sources-picker": HaSourcesPicker;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"source-filters-changed": { value: SourceFilters };
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,7 @@ import type {
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import type { TargetSelector } from "../../data/selector";
|
||||
import {
|
||||
getDataFromPath,
|
||||
getNestedStepConfig,
|
||||
getTracedEntityId,
|
||||
isTriggerPath,
|
||||
} from "../../data/trace";
|
||||
import { getDataFromPath, isTriggerPath } from "../../data/trace";
|
||||
import { getTraceTriggers } from "../../data/trace-tree";
|
||||
import type { TriggerDescriptions } from "../../data/trigger";
|
||||
import { getDeviceTarget } from "../../panels/config/automation/target/get_device_target";
|
||||
@@ -225,12 +220,20 @@ export class HaTracePathDetails extends LitElement {
|
||||
const data: ActionTraceStep[] = paths[curPath];
|
||||
|
||||
// Extract details from this.selected.config child properties used to add 'alias' (to headline), describeCondition and 'entity_id' (to result)
|
||||
const nestedPath = curPath.substring(this.selected.path.length + 1);
|
||||
const currentDetail = getNestedStepConfig(
|
||||
this.selected.config,
|
||||
nestedPath
|
||||
);
|
||||
const tracedEntityId = getTracedEntityId(currentDetail, nestedPath);
|
||||
const nestPath = curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.split("/");
|
||||
let currentDetail: unknown = this.selected.config;
|
||||
for (const part of nestPath) {
|
||||
if (typeof currentDetail !== "object" || currentDetail === null) {
|
||||
break;
|
||||
}
|
||||
const child = (currentDetail as Record<string, unknown>)[part];
|
||||
if (child === undefined || typeof child === "string") {
|
||||
break;
|
||||
}
|
||||
currentDetail = child;
|
||||
}
|
||||
|
||||
parts.push(
|
||||
data.map((trace, idx) => {
|
||||
@@ -325,8 +328,14 @@ export class HaTracePathDetails extends LitElement {
|
||||
: html`<pre>${dump(rest)}</pre>`
|
||||
}
|
||||
${
|
||||
tracedEntityId
|
||||
? html`<pre>entity: ${tracedEntityId}</pre>`
|
||||
typeof currentDetail === "object" &&
|
||||
currentDetail !== null &&
|
||||
"entity_id" in currentDetail &&
|
||||
currentDetail.entity_id &&
|
||||
curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.includes("entity_id")
|
||||
? html`<pre>entity: ${currentDetail.entity_id}</pre>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
@@ -667,10 +676,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--ha-panel-background, var(--primary-background-color))
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
+35
-6
@@ -5,7 +5,10 @@ import type {
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { createDurationData } from "../common/datetime/create_duration_data";
|
||||
import { applyDurationSign } from "../common/datetime/normalize_duration";
|
||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
@@ -524,6 +527,26 @@ export const pickRowConfig = <T extends object>(
|
||||
return config as Partial<T>;
|
||||
};
|
||||
|
||||
// Older sun and calendar triggers set a positive offset with an offset type,
|
||||
// which the backend folds into the sign of the offset.
|
||||
const OFFSET_TYPE_TRIGGER_DOMAINS = ["sun", "calendar"];
|
||||
|
||||
const foldOffsetType = (
|
||||
options: Record<string, unknown>
|
||||
): Record<string, unknown> => {
|
||||
const { offset_type: offsetType, ...folded } = options;
|
||||
const offset = createDurationData(
|
||||
folded.offset as string | number | ForDict | undefined
|
||||
);
|
||||
if (offset && !Object.values(offset).every(Number.isFinite)) {
|
||||
return options;
|
||||
}
|
||||
if (offsetType === "before" && offset) {
|
||||
folded.offset = applyDurationSign(offset, true);
|
||||
}
|
||||
return folded;
|
||||
};
|
||||
|
||||
export const getRowNote = (note: unknown): string | undefined =>
|
||||
typeof note === "string" ? note.trim() || undefined : undefined;
|
||||
|
||||
@@ -551,12 +574,12 @@ export const migrateAutomationTrigger = (
|
||||
delete trigger.platform;
|
||||
}
|
||||
|
||||
if ("options" in trigger) {
|
||||
if (
|
||||
typeof trigger.options === "object" &&
|
||||
trigger.options !== null &&
|
||||
"behavior" in trigger.options
|
||||
) {
|
||||
if (
|
||||
"options" in trigger &&
|
||||
typeof trigger.options === "object" &&
|
||||
trigger.options !== null
|
||||
) {
|
||||
if ("behavior" in trigger.options) {
|
||||
// Deprecated behavior values renamed in 2026; the backend raises a repair
|
||||
// when they are still used, so flag the migration to offer saving.
|
||||
if (trigger.options.behavior === "any") {
|
||||
@@ -571,6 +594,12 @@ export const migrateAutomationTrigger = (
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
"offset_type" in trigger.options &&
|
||||
OFFSET_TYPE_TRIGGER_DOMAINS.includes(computeDomain(trigger.trigger))
|
||||
) {
|
||||
trigger.options = foldOffsetType(trigger.options);
|
||||
}
|
||||
}
|
||||
|
||||
return trigger;
|
||||
|
||||
+24
-25
@@ -1,6 +1,8 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { createDurationData } from "../common/datetime/create_duration_data";
|
||||
import { durationDataToSeconds } from "../common/datetime/duration_to_seconds";
|
||||
import {
|
||||
formatDurationDigital,
|
||||
formatDurationLong,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../common/datetime/format_time";
|
||||
import { normalizeDuration } from "../common/datetime/normalize_duration";
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { sortWeekdays } from "../common/datetime/sort_weekdays";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
@@ -412,16 +415,7 @@ const describeLegacyTrigger = (
|
||||
|
||||
// Sun Trigger
|
||||
if (trigger.trigger === "sun" && trigger.event) {
|
||||
let duration = "";
|
||||
if (trigger.offset) {
|
||||
if (typeof trigger.offset === "number") {
|
||||
duration = secondsToDuration(trigger.offset)!;
|
||||
} else if (typeof trigger.offset === "string") {
|
||||
duration = trigger.offset;
|
||||
} else {
|
||||
duration = JSON.stringify(trigger.offset);
|
||||
}
|
||||
}
|
||||
const duration = formatSunOffset(hass, trigger.offset);
|
||||
|
||||
return hass.localize(
|
||||
trigger.event === "sunset"
|
||||
@@ -831,19 +825,16 @@ const describeLegacyTrigger = (
|
||||
|
||||
let offsetChoice = "other";
|
||||
let offset = "";
|
||||
if (typeof trigger.offset === "string" && trigger.offset) {
|
||||
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||
const parts = trigger.offset.startsWith("-")
|
||||
? trigger.offset.substring(1).split(":")
|
||||
: trigger.offset.split(":");
|
||||
const duration = {
|
||||
hours: parts.length > 0 ? +parts[0] : 0,
|
||||
minutes: parts.length > 1 ? +parts[1] : 0,
|
||||
seconds: parts.length > 2 ? +parts[2] : 0,
|
||||
};
|
||||
offset = formatDurationLong(hass.locale, duration);
|
||||
if (offset === "") {
|
||||
offsetChoice = "other";
|
||||
const duration = createDurationData(trigger.offset);
|
||||
if (duration) {
|
||||
const normalized = normalizeDuration(duration, {
|
||||
enableDay: true,
|
||||
enableSecond: true,
|
||||
enableMillisecond: true,
|
||||
});
|
||||
offset = formatDurationLong(hass.locale, normalized.duration);
|
||||
if (offset) {
|
||||
offsetChoice = normalized.negative ? "before" : "after";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -874,9 +865,17 @@ const formatSunOffset = (
|
||||
if (typeof offset === "string") {
|
||||
return offset;
|
||||
}
|
||||
if (!durationDataToSeconds(offset)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
const formatted = formatDurationDigital(hass.locale, offset);
|
||||
return formatted.startsWith("-") ? formatted : `+${formatted}`;
|
||||
const normalized = normalizeDuration(offset, {
|
||||
enableDay: false,
|
||||
enableSecond: true,
|
||||
enableMillisecond: true,
|
||||
});
|
||||
const formatted = formatDurationDigital(hass.locale, normalized.duration);
|
||||
return `${normalized.negative ? "-" : "+"}${formatted}`;
|
||||
} catch (_e) {
|
||||
return JSON.stringify(offset);
|
||||
}
|
||||
|
||||
@@ -103,20 +103,6 @@ export const subscribeBackupEvents = (
|
||||
{ preCheck }
|
||||
);
|
||||
|
||||
export const getBackupState = async (
|
||||
hass: Pick<HomeAssistant, "connection">
|
||||
): Promise<BackupManagerState> => {
|
||||
let state: BackupManagerState = "idle";
|
||||
const unsub = await subscribeBackupEvents(hass, (event) => {
|
||||
// Skip replayed upload progress events, only the manager state matters
|
||||
if (!("agent_id" in event)) {
|
||||
state = event.manager_state;
|
||||
}
|
||||
});
|
||||
unsub();
|
||||
return state;
|
||||
};
|
||||
|
||||
export const DEFAULT_MANAGER_STATE: ManagerStateEvent = {
|
||||
manager_state: "idle",
|
||||
};
|
||||
@@ -22,9 +22,10 @@ export interface CalendarEvent {
|
||||
title: string;
|
||||
start: string;
|
||||
end?: string;
|
||||
color?: string;
|
||||
contrastColor?: string;
|
||||
className?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
textColor?: string;
|
||||
classNames?: string[];
|
||||
calendar: string;
|
||||
eventData: CalendarEventData;
|
||||
[key: string]: any;
|
||||
@@ -288,12 +289,12 @@ export const normalizeSubscriptionEventData = (
|
||||
start: eventStart,
|
||||
end: eventEnd,
|
||||
title: eventData.summary,
|
||||
color: calendar.backgroundColor,
|
||||
// FullCalendar reads undefined as the text "undefined", and empty as unset
|
||||
contrastColor: calendar.textColor ?? "",
|
||||
backgroundColor: calendar.backgroundColor,
|
||||
borderColor: calendar.backgroundColor,
|
||||
textColor: calendar.textColor,
|
||||
// Only a tentative event is drawn differently. A confirmed event and one
|
||||
// without a status look the same, as most integrations report none.
|
||||
className: eventData.status === "tentative" ? "tentative" : undefined,
|
||||
classNames: eventData.status === "tentative" ? ["tentative"] : undefined,
|
||||
calendar: calendar.entity_id,
|
||||
eventData: normalizedEventData,
|
||||
};
|
||||
|
||||
@@ -710,16 +710,6 @@ export const isNumericEntity = (
|
||||
isNumericSensorDeviceClass(currentState.attributes.device_class)) ||
|
||||
numericStateFromHistory != null;
|
||||
|
||||
// Non-numeric domains that computeHistory gives a unit, so they get a line chart
|
||||
const LINE_CHART_DOMAINS = ["climate", "humidifier", "water_heater", "zone"];
|
||||
|
||||
export const isTimelineEntity = (
|
||||
domain: string,
|
||||
currentState: HassEntity | undefined
|
||||
): boolean =>
|
||||
!isNumericEntity(domain, currentState, undefined) &&
|
||||
!LINE_CHART_DOMAINS.includes(domain);
|
||||
|
||||
export const mergeHistoryResults = (
|
||||
historyResult: HistoryResult,
|
||||
ltsResult?: HistoryResult,
|
||||
|
||||
@@ -329,15 +329,3 @@ export const filterLogbookCompatibleEntities = (entity) => {
|
||||
(domain === "sensor" && isNumericEntity(domain, entity, undefined));
|
||||
return !continuous;
|
||||
};
|
||||
|
||||
export type LogbookCauseGroup =
|
||||
"automation" | "script" | "integration" | "other";
|
||||
|
||||
export const LOGBOOK_CAUSE_GROUPS: LogbookCauseGroup[] = [
|
||||
"automation",
|
||||
"script",
|
||||
"integration",
|
||||
"other",
|
||||
];
|
||||
|
||||
export const logbookUserCauseKey = (userId: string) => `user/${userId}`;
|
||||
@@ -266,65 +266,6 @@ export const getDataFromPath = (
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Follows a trace path relative to a step config (for example
|
||||
* `choose/0/conditions/1`) and returns the deepest config it reaches. A
|
||||
* template condition written as a plain string is returned as that string.
|
||||
*/
|
||||
export const getNestedStepConfig = (config: unknown, path: string): unknown => {
|
||||
let currentDetail = config;
|
||||
const parts = path.split("/");
|
||||
for (const [index, part] of parts.entries()) {
|
||||
if (typeof currentDetail !== "object" || currentDetail === null) {
|
||||
break;
|
||||
}
|
||||
// Core traces each entity of a state condition one level deeper than the
|
||||
// config, as `entity_id/<index>`. The condition itself is the deepest step.
|
||||
if (part === "entity_id") {
|
||||
break;
|
||||
}
|
||||
// Core traces the conditions of an `if` action as `if/condition/<index>`,
|
||||
// while the config keeps them directly in `if`, as a list or as a single
|
||||
// condition.
|
||||
if (part === "condition" && parts[index - 1] === "if") {
|
||||
continue;
|
||||
}
|
||||
// A list of conditions or actions can also be written as a single item,
|
||||
// which Core traces as index 0.
|
||||
if (!Array.isArray(currentDetail) && /^\d+$/.test(part)) {
|
||||
if (part !== "0") {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const child = (currentDetail as Record<string, unknown>)[part];
|
||||
if (child === undefined) {
|
||||
break;
|
||||
}
|
||||
currentDetail = child;
|
||||
}
|
||||
return currentDetail;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the entity a condition trace step such as `entity_id/1` checked.
|
||||
* Core traces each entity of a condition with a list of entities separately.
|
||||
*/
|
||||
export const getTracedEntityId = (
|
||||
config: unknown,
|
||||
path: string
|
||||
): string | undefined => {
|
||||
const match = /(?:^|\/)entity_id\/(\d+)$/.exec(path);
|
||||
if (!match || typeof config !== "object" || config === null) {
|
||||
return undefined;
|
||||
}
|
||||
const entityIds = (config as { entity_id?: unknown }).entity_id;
|
||||
const entityId = Array.isArray(entityIds)
|
||||
? entityIds[Number(match[1])]
|
||||
: entityIds;
|
||||
return typeof entityId === "string" ? entityId : undefined;
|
||||
};
|
||||
|
||||
// It is 'trigger' if manually triggered by the user via UI
|
||||
export const isTriggerPath = (path: string): boolean =>
|
||||
path === "trigger" || strStartsWith(path, "trigger/");
|
||||
|
||||
@@ -229,7 +229,6 @@ export const checkForEntityUpdates = async (
|
||||
};
|
||||
|
||||
// When updating, and entity does not support % show "Installing"
|
||||
// When updating, and entity does support % but has not reported it yet, show "Preparing"
|
||||
// When updating, and entity does support % show "Installing (xx%)"
|
||||
// When update available, show "Update available"
|
||||
// When the latest version is skipped, show the latest version
|
||||
@@ -246,14 +245,6 @@ export const computeUpdateStateDisplay = (
|
||||
// downgrading firmware (installed_version is newer than latest_version).
|
||||
// Show the installing status regardless of state in that case.
|
||||
if (updateIsInstalling(stateObj)) {
|
||||
// The install has not started yet, for example while a backup is
|
||||
// created before the update.
|
||||
if (
|
||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
attributes.update_percentage == null
|
||||
) {
|
||||
return options.localize("ui.card.update.preparing");
|
||||
}
|
||||
const supportsProgress =
|
||||
supportsFeature(stateObj, UpdateEntityFeature.PROGRESS) &&
|
||||
attributes.update_percentage !== null;
|
||||
|
||||
@@ -721,20 +721,11 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
return;
|
||||
}
|
||||
this._progress = undefined;
|
||||
let catchingUp = true;
|
||||
// Fetching re-runs the current step, which external steps can treat as the user returning,
|
||||
// and only progress tasks can finish before the subscription is in place
|
||||
let fetchPending = this._step?.type === "progress";
|
||||
const unsubs = [
|
||||
subscribeDataEntryFlowProgressed(this.hass.connection, (ev) => {
|
||||
if (ev.data.flow_id !== this._step?.flow_id) {
|
||||
return;
|
||||
}
|
||||
if (catchingUp) {
|
||||
// Two concurrent fetches can both advance the flow, so let the catch-up below fetch again
|
||||
fetchPending = true;
|
||||
return;
|
||||
}
|
||||
this._processStep(
|
||||
this._params!.flowConfig.fetchFlow(this.hass, this._step.flow_id)
|
||||
);
|
||||
@@ -745,54 +736,9 @@ class DataEntryFlowDialog extends DirtyStateProviderMixin<
|
||||
this._progress = Math.ceil(ev.data.progress * 100);
|
||||
}),
|
||||
];
|
||||
const unsubscribe = async () => {
|
||||
this._unsubDataEntryFlowProgress = async () => {
|
||||
(await Promise.all(unsubs)).map((unsub) => unsub());
|
||||
};
|
||||
this._unsubDataEntryFlowProgress = unsubscribe;
|
||||
|
||||
// The backend fires the progressed event even if we have not subscribed yet,
|
||||
// so fetch the flow once in case its progress task already finished
|
||||
try {
|
||||
await Promise.all(unsubs);
|
||||
while (fetchPending) {
|
||||
fetchPending = false;
|
||||
const step = this._step;
|
||||
if (
|
||||
this._unsubDataEntryFlowProgress !== unsubscribe ||
|
||||
!step ||
|
||||
(step.type !== "external" && step.type !== "progress")
|
||||
) {
|
||||
break;
|
||||
}
|
||||
// A failed fetch only ends the loop if no progressed event arrived while it was in flight
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const current = await this._params!.flowConfig.fetchFlow(
|
||||
this.hass,
|
||||
step.flow_id
|
||||
).catch(() => undefined);
|
||||
if (
|
||||
current &&
|
||||
this._step === step &&
|
||||
!(
|
||||
current.type === step.type &&
|
||||
"step_id" in current &&
|
||||
current.step_id === step.step_id &&
|
||||
// Progress steps can move on to another task without changing step_id
|
||||
(current.type !== "progress" ||
|
||||
(step.type === "progress" &&
|
||||
current.progress_action === step.progress_action))
|
||||
)
|
||||
) {
|
||||
this._progress = undefined;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this._processStep(current);
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
// Keep waiting for the progressed event
|
||||
} finally {
|
||||
catchingUp = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _focusFormStep = async (): Promise<void> => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
uiContext,
|
||||
} from "../../data/context";
|
||||
import type { DataEntryFlowProgress } from "../../data/data_entry_flow";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import {
|
||||
fetchESPHomeDeviceCapabilities,
|
||||
type ESPHomeDeviceCapabilities,
|
||||
@@ -208,19 +206,12 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
this._loadStarted = true;
|
||||
this._capabilities = this.params.capabilities;
|
||||
this._load();
|
||||
} else if (this._loadStarted && this._capabilities?.serial_proxies.length) {
|
||||
if (changedProps.has("_configEntries")) {
|
||||
this._refreshSerialPorts();
|
||||
} else if (
|
||||
changedProps.has("_states") &&
|
||||
this._unavailableEntityCount(this._states) !==
|
||||
this._unavailableEntityCount(
|
||||
changedProps.get("_states") as ContextType<typeof statesContext>
|
||||
)
|
||||
) {
|
||||
// The device reconnected or dropped, which changes its scanned ports
|
||||
this._refreshSerialPortsDebounced();
|
||||
}
|
||||
} else if (
|
||||
this._loadStarted &&
|
||||
changedProps.has("_configEntries") &&
|
||||
this._capabilities?.serial_proxies.length
|
||||
) {
|
||||
this._refreshSerialPorts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,25 +579,14 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
return this._configEntries.find((entry) => entry.entry_id === entryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Undefined when USB usage has not been loaded. A port of a connected device
|
||||
* is always in the scan, so a missing or absent port means it is offline.
|
||||
* Without the usb integration nothing is scanned, so a missing port says nothing.
|
||||
*/
|
||||
private _serialPortUsage(url: string) {
|
||||
/** Undefined when USB usage has not been loaded. An empty array is a real scan. */
|
||||
private _serialConsumers(url: string) {
|
||||
if (!this._serialPorts) {
|
||||
return undefined;
|
||||
}
|
||||
const usage = this._serialPorts.find((port) => port.device === url);
|
||||
return {
|
||||
consumers: usage?.consumers ?? [],
|
||||
offline: usage
|
||||
? !usage.present
|
||||
: Boolean(
|
||||
this._hassConfig &&
|
||||
isComponentLoaded(this._hassConfig.config, "usb")
|
||||
),
|
||||
};
|
||||
return (
|
||||
this._serialPorts.find((port) => port.device === url)?.consumers ?? []
|
||||
);
|
||||
}
|
||||
|
||||
private _renderSerialPortList() {
|
||||
@@ -618,8 +598,7 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
return html`
|
||||
<ul class="ports">
|
||||
${ports.map((port) => {
|
||||
const usage = this._serialPortUsage(port.url);
|
||||
const consumers = usage?.consumers;
|
||||
const consumers = this._serialConsumers(port.url);
|
||||
return html`
|
||||
<li>
|
||||
<span class="port-text">
|
||||
@@ -640,31 +619,23 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
}
|
||||
</span>
|
||||
${
|
||||
usage === undefined
|
||||
consumers === undefined
|
||||
? nothing
|
||||
: usage.offline
|
||||
: consumers.length
|
||||
? html`
|
||||
<span class="chip">
|
||||
<span class="chip active">
|
||||
${localize(
|
||||
"ui.panel.config.devices.esphome.setup_serial_offline"
|
||||
"ui.panel.config.devices.esphome.setup_serial_configured"
|
||||
)}
|
||||
</span>
|
||||
`
|
||||
: usage.consumers.length
|
||||
? html`
|
||||
<span class="chip active">
|
||||
${localize(
|
||||
"ui.panel.config.devices.esphome.setup_serial_configured"
|
||||
)}
|
||||
</span>
|
||||
`
|
||||
: html`
|
||||
<ha-button size="s" @click=${this._setupSerialPort}>
|
||||
${localize(
|
||||
"ui.panel.config.devices.esphome.setup_action"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-button size="s" @click=${this._setupSerialPort}>
|
||||
${localize(
|
||||
"ui.panel.config.devices.esphome.setup_action"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
</li>
|
||||
`;
|
||||
@@ -829,27 +800,6 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
}
|
||||
}
|
||||
|
||||
/** ESPHome marks the entities of a disconnected device unavailable. */
|
||||
private _unavailableEntityCount(
|
||||
states: ContextType<typeof statesContext> | undefined
|
||||
): number {
|
||||
if (!this.params || !this._entities || !states) {
|
||||
return 0;
|
||||
}
|
||||
const deviceId = this.params.deviceId;
|
||||
return Object.values(this._entities).filter(
|
||||
(entity) =>
|
||||
entity.device_id === deviceId &&
|
||||
states[entity.entity_id]?.state === UNAVAILABLE
|
||||
).length;
|
||||
}
|
||||
|
||||
private _refreshSerialPortsDebounced = debounce(() => {
|
||||
if (this.isConnected) {
|
||||
this._refreshSerialPorts();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
private async _refreshSerialPorts() {
|
||||
const capabilities = this._capabilities;
|
||||
if (!capabilities) {
|
||||
@@ -1265,8 +1215,6 @@ class DialogESPHomeDeviceSetup extends DialogMixin<ESPHomeDeviceSetupDialogParam
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: 1;
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.chip.active {
|
||||
background: var(--success-color);
|
||||
|
||||
@@ -39,14 +39,12 @@ import {
|
||||
updateReleaseNotes,
|
||||
} from "../../../data/update";
|
||||
import type {
|
||||
CurrentUser,
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantFormatters,
|
||||
} from "../../../types";
|
||||
import { showAlertDialog } from "../../generic/show-dialog-box";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showLogsDialog } from "../../../panels/config/logs/show-dialog-logs";
|
||||
import "../components/update/ha-more-info-update-backup";
|
||||
import type { HaMoreInfoUpdateBackup } from "../components/update/ha-more-info-update-backup";
|
||||
|
||||
@@ -84,13 +82,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
})
|
||||
private _config!: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, CurrentUser | undefined>({
|
||||
transformer: ({ user }) => user,
|
||||
})
|
||||
private _user?: CurrentUser;
|
||||
|
||||
@state() private _releaseNotes?: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -295,20 +286,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
"ui.dialogs.more_info_control.update.install_failed",
|
||||
{ error: this._installError }
|
||||
)}
|
||||
${
|
||||
this._canShowSupervisorLogs()
|
||||
? html`<ha-button
|
||||
slot="action"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleShowSupervisorLogs}
|
||||
>
|
||||
${this._localize(
|
||||
"ui.dialogs.more_info_control.update.supervisor_logs"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
@@ -439,41 +416,11 @@ class MoreInfoUpdate extends LitElement {
|
||||
{ error: message }
|
||||
),
|
||||
duration: 10000,
|
||||
action: this._canShowSupervisorLogs()
|
||||
? {
|
||||
text: this._localize(
|
||||
"ui.dialogs.more_info_control.update.supervisor_logs"
|
||||
),
|
||||
action: () =>
|
||||
this._showSupervisorLogs(toastHost as HTMLElement),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supervisor errors only say "Check Supervisor logs for details".
|
||||
private _canShowSupervisorLogs(): boolean {
|
||||
return (
|
||||
this._entityEntry?.platform === "hassio" && Boolean(this._user?.is_admin)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleShowSupervisorLogs(): void {
|
||||
this._showSupervisorLogs(this);
|
||||
}
|
||||
|
||||
// The toast outlives this element, so it opens the dialog from its own host.
|
||||
private _showSupervisorLogs(host: HTMLElement): void {
|
||||
showLogsDialog(host, {
|
||||
provider: "supervisor",
|
||||
title: this._localize(
|
||||
"ui.dialogs.more_info_control.update.supervisor_logs"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _isShown(): boolean {
|
||||
if (!this.isConnected) {
|
||||
return false;
|
||||
|
||||
@@ -21,10 +21,8 @@ import "../../components/item/ha-list-item-button";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import "../../components/progress/ha-progress-bar";
|
||||
import {
|
||||
getBackupState,
|
||||
type BackupManagerState,
|
||||
} from "../../data/backup_manager";
|
||||
import { fetchBackupInfo } from "../../data/backup";
|
||||
import type { BackupManagerState } from "../../data/backup_manager";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
@@ -76,7 +74,8 @@ class DialogRestart extends LitElement {
|
||||
|
||||
private async _loadBackupState() {
|
||||
try {
|
||||
return await getBackupState(this.hass);
|
||||
const { state: backupState } = await fetchBackupInfo(this.hass);
|
||||
return backupState;
|
||||
} catch (_err) {
|
||||
// Do nothing
|
||||
return "idle";
|
||||
|
||||
@@ -86,6 +86,7 @@ class HassSubpage extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -961,10 +961,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
padding-right: calc(16px + var(--data-table-inset-right, 0px));
|
||||
gap: var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -309,6 +309,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) {
|
||||
|
||||
@@ -198,20 +198,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
/* allow a light tap highlight on the actual interface elements */
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
partial-panel-resolver {
|
||||
display: block;
|
||||
height: 100%;
|
||||
isolation: isolate;
|
||||
}
|
||||
partial-panel-resolver::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background: var(--ha-panel-background, var(--primary-background-color));
|
||||
background-attachment: scroll !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DateClickInfo,
|
||||
EventClickInfo,
|
||||
} from "fullcalendar";
|
||||
import { Calendar, joinClassNames } from "fullcalendar";
|
||||
import allLocales from "fullcalendar/locales-all";
|
||||
import dayGridPlugin from "fullcalendar/daygrid";
|
||||
import interactionPlugin from "fullcalendar/interaction";
|
||||
import listPlugin from "fullcalendar/list";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import listPlugin from "@fullcalendar/list";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import {
|
||||
mdiPlus,
|
||||
mdiViewAgenda,
|
||||
@@ -18,7 +15,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoize from "memoize-one";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
@@ -37,10 +34,6 @@ import type {
|
||||
} from "../../data/calendar";
|
||||
import { CalendarEntityFeature } from "../../data/calendar";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import {
|
||||
fullCalendarStyles,
|
||||
fullCalendarTheme,
|
||||
} from "../../resources/fullcalendar-theme";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type {
|
||||
CalendarViewChanged,
|
||||
@@ -61,56 +54,18 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
type EventTimeFormatter = Extract<
|
||||
NonNullable<CalendarOptions["eventTimeFormat"]>,
|
||||
(...args: any[]) => string
|
||||
>;
|
||||
|
||||
const weekTitleFormat: CalendarOptions["titleFormat"] = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
};
|
||||
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
headerToolbar: false,
|
||||
plugins: [fullCalendarTheme, dayGridPlugin, listPlugin, interactionPlugin],
|
||||
plugins: [dayGridPlugin, listPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
height: "var(--calendar-height)",
|
||||
dayMaxEventRows: true,
|
||||
height: "parent",
|
||||
handleWindowResize: false,
|
||||
locales: allLocales,
|
||||
viewClass: ({ view }) => `ha-fc-${view.type}`,
|
||||
dayHeaderClass: ({ inPopover }) => (inPopover ? "ha-fc-popover-header" : ""),
|
||||
dayRowClass: "ha-fc-day-row",
|
||||
dayCellClass: ({ isPast, inPopover }) =>
|
||||
isPast && !inPopover ? "ha-fc-day-past" : "",
|
||||
dayCellTopClass: "ha-fc-day-top",
|
||||
dayCellTopInnerClass: ({ isToday }) =>
|
||||
joinClassNames("ha-fc-day-number", isToday && "ha-fc-day-number-today"),
|
||||
eventClass: "ha-fc-event",
|
||||
rowEventClass: "ha-fc-row-event",
|
||||
rowEventInnerClass: "ha-fc-row-event-inner",
|
||||
moreLinkClass: "ha-fc-more-link",
|
||||
popoverClass: "ha-fc-popover",
|
||||
listDayClass: "ha-fc-list-day",
|
||||
listDayHeaderClass: "ha-fc-list-day-header",
|
||||
listDayHeaderInnerClass: "ha-fc-list-day-text",
|
||||
views: {
|
||||
dayGrid: {
|
||||
listItemEventClass: "ha-fc-dot-event",
|
||||
listItemEventBeforeClass: "ha-fc-dot",
|
||||
listItemEventInnerClass: "ha-fc-dot-event-text",
|
||||
},
|
||||
dayGridWeek: {
|
||||
titleFormat: weekTitleFormat,
|
||||
},
|
||||
list: {
|
||||
listItemEventClass: "ha-fc-list-event",
|
||||
listItemEventBeforeClass: "ha-fc-dot ha-fc-dot-large",
|
||||
},
|
||||
listWeek: {
|
||||
type: "list",
|
||||
duration: { days: 7 },
|
||||
titleFormat: weekTitleFormat,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -156,11 +111,19 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
@state() private _activeView = this.initialView;
|
||||
|
||||
@query("style[data-fullcalendar]") private _fullCalendarStyle?: HTMLElement;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => this.calendar?.updateSize(),
|
||||
});
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._clearMidnightRefreshTimeout();
|
||||
super.disconnectedCallback();
|
||||
this.calendar?.destroy();
|
||||
this.calendar = undefined;
|
||||
this._fullCalendarStyle?.remove();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
@@ -356,7 +319,6 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
if (changedProps.has("autoHeight")) {
|
||||
this.calendar.setOption("height", this._height);
|
||||
this.calendar.setOption("dayMaxEventRows", !this.autoHeight);
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant;
|
||||
@@ -371,9 +333,18 @@ export class HAFullCalendar extends LitElement {
|
||||
this._activeView = this.initialView;
|
||||
}
|
||||
|
||||
private _loadCalendar(initialView: FullCalendarView) {
|
||||
private async _loadCalendar(initialView: FullCalendarView) {
|
||||
const luxonPlugin =
|
||||
this.hass.locale.time_zone === TimeZone.local
|
||||
? undefined
|
||||
: (await import("@fullcalendar/luxon3")).default;
|
||||
|
||||
const config: CalendarOptions = {
|
||||
...defaultFullCalendarConfig,
|
||||
plugins:
|
||||
this.hass.locale.time_zone === TimeZone.local
|
||||
? defaultFullCalendarConfig.plugins
|
||||
: [...defaultFullCalendarConfig.plugins!, luxonPlugin!],
|
||||
locale: this.hass.language,
|
||||
timeZone:
|
||||
this.hass.locale.time_zone === TimeZone.local
|
||||
@@ -382,14 +353,21 @@ export class HAFullCalendar extends LitElement {
|
||||
firstDay: firstWeekdayIndex(this.hass.locale),
|
||||
initialView,
|
||||
height: this._height,
|
||||
// Rows grow to fit all events when the height is automatic
|
||||
dayMaxEventRows: !this.autoHeight,
|
||||
eventDisplay: this.eventDisplay,
|
||||
eventTimeFormat: this._formatEventTime,
|
||||
eventTimeFormat: {
|
||||
hour: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
|
||||
minute: useAmPm(this.hass.locale) ? "numeric" : "2-digit",
|
||||
hour12: useAmPm(this.hass.locale),
|
||||
},
|
||||
};
|
||||
|
||||
config.dateClick = (info) => this._handleDateClick(info);
|
||||
config.eventClick = (info) => this._handleEventClick(info);
|
||||
// fullcalendar sets the event colors only inline, where styles cannot mix them
|
||||
config.eventDidMount = ({ el, event }) => {
|
||||
el.style.setProperty("--event-color", event.borderColor);
|
||||
el.style.setProperty("--event-text-color", event.textColor);
|
||||
};
|
||||
|
||||
this.calendar = new Calendar(
|
||||
this.shadowRoot!.getElementById("calendar")!,
|
||||
@@ -433,7 +411,7 @@ export class HAFullCalendar extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleEventClick(info: EventClickInfo): void {
|
||||
private _handleEventClick(info): void {
|
||||
const entityStateObj = this.hass.states[info.event.extendedProps.calendar];
|
||||
const canEdit =
|
||||
entityStateObj &&
|
||||
@@ -444,7 +422,7 @@ export class HAFullCalendar extends LitElement {
|
||||
showCalendarEventDetailDialog(this, {
|
||||
calendarId: info.event.extendedProps.calendar,
|
||||
entry: info.event.extendedProps.eventData,
|
||||
color: info.event.color,
|
||||
color: info.event.backgroundColor,
|
||||
updated: () => {
|
||||
this._fireViewChanged();
|
||||
},
|
||||
@@ -453,7 +431,7 @@ export class HAFullCalendar extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleDateClick(info: DateClickInfo): void {
|
||||
private _handleDateClick(info): void {
|
||||
if (info.view.type !== "dayGridMonth") {
|
||||
return;
|
||||
}
|
||||
@@ -544,34 +522,6 @@ export class HAFullCalendar extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
// FullCalendar 7 drops the space before AM/PM and shortens time ranges
|
||||
private _formatEventTime: EventTimeFormatter = ({
|
||||
start,
|
||||
end,
|
||||
localeCodes,
|
||||
timeZone,
|
||||
}) => {
|
||||
const formatter = this._eventTimeFormatter(
|
||||
localeCodes,
|
||||
timeZone,
|
||||
useAmPm(this.hass.locale)
|
||||
);
|
||||
const startText = formatter.format(start.instantMs);
|
||||
return end
|
||||
? `${startText} - ${formatter.format(end.instantMs)}`
|
||||
: startText;
|
||||
};
|
||||
|
||||
private _eventTimeFormatter = memoize(
|
||||
(localeCodes: string[], timeZone: string, amPm: boolean) =>
|
||||
new Intl.DateTimeFormat(localeCodes, {
|
||||
hour: amPm ? "numeric" : "2-digit",
|
||||
minute: amPm ? "numeric" : "2-digit",
|
||||
hour12: amPm,
|
||||
timeZone: timeZone === "local" ? undefined : timeZone,
|
||||
})
|
||||
);
|
||||
|
||||
private _calendarTimeZone(): string {
|
||||
return resolveTimeZone(
|
||||
this.hass.locale.time_zone,
|
||||
@@ -622,12 +572,11 @@ export class HAFullCalendar extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
fullCalendarStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--fc-classic-today: transparent;
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -660,6 +609,10 @@ export class HAFullCalendar extends LitElement {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -716,65 +669,116 @@ export class HAFullCalendar extends LitElement {
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
height: var(--calendar-height);
|
||||
--fc-neutral-bg-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
--fc-list-event-hover-bg-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
--fc-theme-standard-border-color: var(--divider-color);
|
||||
--fc-border-color: var(--divider-color);
|
||||
--fc-page-bg-color: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
}
|
||||
|
||||
.ha-fc-listWeek {
|
||||
border-style: none;
|
||||
a {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.ha-fc-day-top {
|
||||
justify-content: center;
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-width: var(--calendar-border-width, 1px);
|
||||
border-radius: var(
|
||||
--calendar-border-radius,
|
||||
var(--mdc-shape-small, 4px)
|
||||
);
|
||||
}
|
||||
|
||||
.fc-theme-standard td {
|
||||
border-bottom-left-radius: var(--mdc-shape-small, 4px);
|
||||
border-bottom-right-radius: var(--mdc-shape-small, 4px);
|
||||
}
|
||||
|
||||
.fc-scrollgrid-section-header td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
th.fc-col-header-cell.fc-day {
|
||||
background-color: var(--table-header-background-color);
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.fc-daygrid-dot-event:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-top {
|
||||
text-align: center;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ha-fc-day-top:empty,
|
||||
.ha-fc-day-row:first-child .ha-fc-day-top {
|
||||
table.fc-scrollgrid-sync-table
|
||||
tbody
|
||||
tr:first-child
|
||||
.fc-daygrid-day-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ha-fc-day-number {
|
||||
margin: 0;
|
||||
padding: 3px;
|
||||
a.fc-daygrid-day-number {
|
||||
float: none !important;
|
||||
font-size: var(--ha-font-size-s);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ha-fc-day-number-today {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.fc .fc-daygrid-day-number {
|
||||
padding: 3px !important;
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day.fc-day-today {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
td.fc-day-today .fc-daygrid-day-number {
|
||||
height: 26px;
|
||||
min-width: 24px;
|
||||
box-sizing: border-box;
|
||||
color: var(--text-primary-color);
|
||||
color: var(--text-primary-color) !important;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
width: max-content;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.ha-fc-day-past .ha-fc-event,
|
||||
.ha-fc-day-past .ha-fc-more-link {
|
||||
opacity: 0.5;
|
||||
.fc-daygrid-day-events {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.ha-fc-event {
|
||||
.fc-event {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ha-fc-row-event-inner,
|
||||
.ha-fc-dot-event-text {
|
||||
line-height: var(--ha-line-height-normal);
|
||||
}
|
||||
|
||||
.ha-fc-dot-event:hover,
|
||||
.ha-fc-list-event:hover {
|
||||
background-color: transparent;
|
||||
.fc-daygrid-block-event .fc-event-main {
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.tentative {
|
||||
--tentative-tint: color-mix(
|
||||
in srgb,
|
||||
var(--event-color) 25%,
|
||||
transparent
|
||||
);
|
||||
/* In the text color, so the stripes show on every calendar color */
|
||||
--tentative-stripe: color-mix(
|
||||
in srgb,
|
||||
@@ -785,18 +789,16 @@ export class HAFullCalendar extends LitElement {
|
||||
|
||||
/* A dot keeps its size and color and gets stripes in the color that
|
||||
contrasts with it, 1.5px wide every 4px */
|
||||
.tentative > .ha-fc-dot {
|
||||
.tentative .fc-daygrid-event-dot,
|
||||
.tentative .fc-list-event-dot {
|
||||
--tentative-dot-stripe: color-mix(
|
||||
in srgb,
|
||||
var(--fc-event-contrast-color) 80%,
|
||||
var(--event-text-color, #fff) 80%,
|
||||
transparent
|
||||
);
|
||||
/* A long title would otherwise squeeze the dot into an oval */
|
||||
flex-shrink: 0;
|
||||
width: var(--fc-classic-small-dot-width);
|
||||
height: var(--fc-classic-small-dot-width);
|
||||
border: none;
|
||||
background-color: var(--fc-event-color);
|
||||
border-radius: 50%;
|
||||
background-color: var(--event-color);
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
var(--tentative-dot-stripe) 0 9.375%,
|
||||
@@ -810,18 +812,21 @@ export class HAFullCalendar extends LitElement {
|
||||
background-position: 2px 0;
|
||||
}
|
||||
|
||||
.tentative > .ha-fc-dot-large {
|
||||
width: var(--fc-classic-large-dot-width);
|
||||
height: var(--fc-classic-large-dot-width);
|
||||
.tentative .fc-daygrid-event-dot {
|
||||
/* A long title would otherwise squeeze the dot into an oval */
|
||||
flex-shrink: 0;
|
||||
width: var(--fc-daygrid-event-dot-width, 8px);
|
||||
height: var(--fc-daygrid-event-dot-width, 8px);
|
||||
}
|
||||
|
||||
.ha-fc-row-event.tentative {
|
||||
border-color: var(--fc-event-color);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--fc-event-color) 25%,
|
||||
transparent
|
||||
);
|
||||
.tentative .fc-list-event-dot {
|
||||
width: var(--fc-list-event-dot-width, 10px);
|
||||
height: var(--fc-list-event-dot-width, 10px);
|
||||
}
|
||||
|
||||
/* fullcalendar sets the colors as inline styles */
|
||||
.fc-h-event.tentative {
|
||||
background-color: var(--tentative-tint) !important;
|
||||
/* A tile of whole pixels puts every stripe on the same pixels,
|
||||
2.8px wide every 14px */
|
||||
background-image: linear-gradient(
|
||||
@@ -835,62 +840,114 @@ export class HAFullCalendar extends LitElement {
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
|
||||
.tentative > .ha-fc-row-event-inner {
|
||||
color: var(--primary-text-color);
|
||||
.fc-h-event.tentative .fc-event-main {
|
||||
color: var(--primary-text-color) !important;
|
||||
}
|
||||
|
||||
/* Forced colors drop the stripes and the fill of a dot, so a tentative
|
||||
event falls back to its outline */
|
||||
@media (forced-colors: active) {
|
||||
.ha-fc-row-event.tentative {
|
||||
.fc-h-event.tentative {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.tentative > .ha-fc-dot {
|
||||
.tentative .fc-daygrid-event-dot,
|
||||
.tentative .fc-list-event-dot {
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ha-fc-popover {
|
||||
background-color: var(--primary-background-color);
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
.fc-day-past .fc-daygrid-day-events {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ha-fc-popover-header {
|
||||
background-color: var(--secondary-background-color);
|
||||
.fc-icon-x:before {
|
||||
font-family: var(--ha-font-family-body);
|
||||
content: "X";
|
||||
}
|
||||
|
||||
.ha-fc-list-day {
|
||||
border-bottom-style: none;
|
||||
.fc-popover {
|
||||
background-color: var(--primary-background-color) !important;
|
||||
}
|
||||
|
||||
.ha-fc-list-day-header {
|
||||
margin-bottom: 0;
|
||||
border-bottom-style: none;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
background: var(--fc-classic-background);
|
||||
.fc-popover-header {
|
||||
background-color: var(--secondary-background-color) !important;
|
||||
}
|
||||
|
||||
.ha-fc-list-day-text {
|
||||
.fc-theme-standard .fc-list-day-frame {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.fc-list.fc-view,
|
||||
.fc-list-event.fc-event td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-list-day.fc-day th {
|
||||
border-bottom: none;
|
||||
border-top: 1px solid var(--fc-theme-standard-border-color, #ddd) !important;
|
||||
}
|
||||
|
||||
.fc-list-day-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
.ha-fc-list-event {
|
||||
padding-block: 12px;
|
||||
border-top-style: none;
|
||||
.fc-list-day-side-text {
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([narrow]) .ha-fc-dayGridMonth .ha-fc-dot-event {
|
||||
justify-content: center;
|
||||
.fc-list-table td,
|
||||
.fc-list-day-frame {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
:host([narrow]) .ha-fc-dayGridMonth .ha-fc-dot-event-text {
|
||||
:host([narrow])
|
||||
.fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-time,
|
||||
:host([narrow])
|
||||
.fc-dayGridMonth-view
|
||||
.fc-daygrid-dot-event
|
||||
.fc-event-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-event-harness {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-daygrid-day-events {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
:host([narrow]) .fc-dayGridMonth-view .fc-scrollgrid-sync-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fc-scroller::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.fc-scroller::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
.fc-scroller {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ import { getAppDisplayName } from "../../common/app";
|
||||
import "../../components/supervisor-apps-state";
|
||||
import "../../components/supervisor-apps-tag";
|
||||
import "../components/supervisor-app-metric";
|
||||
import { showLogsDialog } from "../../../logs/show-dialog-logs";
|
||||
import { extractChangelog } from "../util/supervisor-app";
|
||||
import "./supervisor-app-system-managed";
|
||||
|
||||
@@ -549,17 +548,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
<div class="description">
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this._error}
|
||||
<ha-button
|
||||
slot="action"
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._showSupervisorLogs}
|
||||
>
|
||||
${this.i18n.localize("ui.panel.config.logs.supervisor_logs")}
|
||||
</ha-button>
|
||||
</ha-alert>`
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<div class="description-text">${this._currentAddon.description}</div>
|
||||
@@ -1324,7 +1313,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
dismissText: this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.view_supervisor_logs"
|
||||
),
|
||||
cancel: () => this._showSupervisorLogs(),
|
||||
cancel: () => navigate("/config/logs?provider=supervisor"),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
@@ -1569,14 +1558,6 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
// Supervisor errors often only say "Check Supervisor logs for details".
|
||||
private _showSupervisorLogs() {
|
||||
showLogsDialog(this, {
|
||||
provider: "supervisor",
|
||||
title: this.i18n.localize("ui.panel.config.logs.supervisor_logs"),
|
||||
});
|
||||
}
|
||||
|
||||
private _openUpdate() {
|
||||
showMoreInfoDialog(this, { entityId: this._updateEntityId! });
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
supervisor-apps-repository {
|
||||
margin-top: 24px;
|
||||
|
||||
@@ -257,6 +257,7 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
ha-card {
|
||||
@@ -276,13 +277,10 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
}
|
||||
|
||||
ha-input-search {
|
||||
|
||||
@@ -174,6 +174,7 @@ export class HaConfigAppsRegistries extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
|
||||
@@ -264,6 +264,7 @@ export class HaConfigAppsRepositories extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
}
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
|
||||
@@ -89,14 +89,12 @@ type AreaQuickLinkKey =
|
||||
const NAVIGATION_ACTIONS: {
|
||||
value: string;
|
||||
path: string;
|
||||
params?: string;
|
||||
icon: string;
|
||||
countKey: AreaQuickLinkKey;
|
||||
}[] = [
|
||||
{
|
||||
value: "navigate-devices",
|
||||
path: "/config/devices/dashboard",
|
||||
params: "&status=disabled",
|
||||
icon: mdiDevices,
|
||||
countKey: "devices",
|
||||
},
|
||||
@@ -658,9 +656,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${NAVIGATION_ACTIONS.filter(
|
||||
(action) => quickLinkCounts[action.countKey]
|
||||
).map(
|
||||
${NAVIGATION_ACTIONS.map(
|
||||
(action) => html`
|
||||
<ha-dropdown-item value=${action.value}>
|
||||
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
|
||||
@@ -672,13 +668,8 @@ class HaConfigAreaPage extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
${
|
||||
NAVIGATION_ACTIONS.some(
|
||||
(action) => quickLinkCounts[action.countKey]
|
||||
)
|
||||
? html`<wa-divider></wa-divider>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item value="edit" .data=${area}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}> </ha-svg-icon>
|
||||
@@ -854,9 +845,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
|
||||
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
|
||||
if (navAction) {
|
||||
navigate(
|
||||
`${navAction.path}?historyBack=1&area=${this.areaId}${navAction.params ?? ""}`
|
||||
);
|
||||
navigate(`${navAction.path}?historyBack=1&area=${this.areaId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -184,10 +184,9 @@ const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "integrationGroups"];
|
||||
|
||||
const DYNAMIC_TO_GENERIC = new Set([`${DYNAMIC_PREFIX}event`]);
|
||||
|
||||
// Group keys surfaced in the "Other" section of the "by target" tab because
|
||||
// Group keys surfaced as their own section in the "by target" tab because
|
||||
// their elements have no target (time/calendar/schedule, sun). Picking one
|
||||
// drills into its items, like selecting the matching group in the "by type" tab.
|
||||
// People are shown in the same section by ha-automation-add-from-target.
|
||||
const TIME_LOCATION_GROUPS = ["time", "sun"];
|
||||
|
||||
type CollectionGroupType = "helper" | "dynamic" | "integration";
|
||||
@@ -1986,7 +1985,7 @@ class DialogAddAutomationElement
|
||||
if (type !== "trigger" && type !== "condition") {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.localize("ui.panel.config.automation.editor.other");
|
||||
return this.hass.localize("ui.panel.config.automation.editor.time_sun");
|
||||
}
|
||||
|
||||
private _getTimeLocationGroups = memoizeOne(
|
||||
@@ -2205,10 +2204,6 @@ class DialogAddAutomationElement
|
||||
return this.hass.localize("ui.panel.config.automation.editor.services");
|
||||
}
|
||||
|
||||
if (targetId === undefined && targetType === "person") {
|
||||
return this.hass.localize("ui.panel.config.person.caption");
|
||||
}
|
||||
|
||||
if (targetId === undefined && targetType === "device") {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.unassigned_entities"
|
||||
|
||||
+41
-129
@@ -3,7 +3,7 @@ import type WaTreeItem from "@home-assistant/webawesome/dist/components/tree-ite
|
||||
import "@home-assistant/webawesome/dist/components/tree/tree";
|
||||
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiAccountMultiple, mdiTextureBox } from "@mdi/js";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -20,7 +20,6 @@ import { consume } from "../../../../common/decorators/consume";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeEntityNameParts } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { getDeviceAreaId } from "../../../../common/entity/context/get_device_context";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
@@ -71,15 +70,10 @@ import type { AddAutomationElementListItem } from "../add-automation-element-dia
|
||||
import type { AddAutomationElementDialogParams } from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-add-element-paste";
|
||||
|
||||
// All person entities are also listed under "People" in the "Other" section
|
||||
// next to time and sun (triggers/conditions only), wherever they are assigned.
|
||||
const PERSON_TARGET = `person${TARGET_SEPARATOR}`;
|
||||
|
||||
interface Level1Entries {
|
||||
open: boolean;
|
||||
areas?: Record<string, Level2Entries>;
|
||||
devices?: Record<string, Level3Entries>;
|
||||
entities?: string[];
|
||||
}
|
||||
|
||||
interface Level2Entries {
|
||||
@@ -220,11 +214,9 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
${this._renderFloors(this.narrow, this._entries, this.value)}
|
||||
${this._renderTimeLocation(
|
||||
this.narrow,
|
||||
this._entries,
|
||||
this.timeLocationLabel,
|
||||
this.timeLocationGroups,
|
||||
this.selectedGroup,
|
||||
this.value
|
||||
this.selectedGroup
|
||||
)}
|
||||
${this._renderUnassigned(this.narrow, this._entries, this.value)}
|
||||
${this._renderLabels(
|
||||
@@ -309,10 +301,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
if (valueType === "person" && !valueId) {
|
||||
return this._renderEntities(entries[PERSON_TARGET]?.entities, false);
|
||||
}
|
||||
|
||||
if (valueType === "device" || valueType === "helper") {
|
||||
const { devices } = entries[`${valueType}${TARGET_SEPARATOR}`];
|
||||
return this._renderDomains(
|
||||
@@ -402,83 +390,42 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
private _renderTimeLocation = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
entries: Record<string, Level1Entries>,
|
||||
label?: string,
|
||||
groups?: AddAutomationElementListItem[],
|
||||
selectedGroup?: string,
|
||||
// only used to invalidate the memoized selected state of the people
|
||||
_value?: SingleHassServiceTarget
|
||||
selectedGroup?: string
|
||||
) => {
|
||||
const people = entries[PERSON_TARGET];
|
||||
if (!label || (!groups?.length && !people)) {
|
||||
if (!label || !groups?.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const peopleItem = people
|
||||
? this._renderItem(
|
||||
this._i18n.localize("ui.panel.config.person.caption"),
|
||||
PERSON_TARGET,
|
||||
true,
|
||||
false,
|
||||
!people.open && !!people.entities!.length,
|
||||
people.open,
|
||||
(slot) =>
|
||||
html`<ha-svg-icon
|
||||
slot=${ifDefined(slot)}
|
||||
.path=${mdiAccountMultiple}
|
||||
></ha-svg-icon>`,
|
||||
people.open ? this._renderEntities(people.entities!) : undefined
|
||||
)
|
||||
: nothing;
|
||||
|
||||
// With an expandable item in the section, render everything as one
|
||||
// tree so the leaf rows reserve the chevron space and icons line up.
|
||||
const asTree = !narrow && !!people;
|
||||
|
||||
const groupItems = groups?.map((group) => {
|
||||
const icon = group.icon
|
||||
? html`<span slot=${ifDefined(asTree ? undefined : "start")}
|
||||
>${group.icon}</span
|
||||
>`
|
||||
: group.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot=${ifDefined(asTree ? undefined : "start")}
|
||||
.path=${group.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing;
|
||||
|
||||
return asTree
|
||||
? html`<wa-tree-item
|
||||
.value=${group.key}
|
||||
.selected=${group.key === selectedGroup}
|
||||
.title=${group.name}
|
||||
>
|
||||
${icon} ${group.name}
|
||||
</wa-tree-item>`
|
||||
: html`<ha-list-item-button
|
||||
.value=${group.key}
|
||||
@click=${this._selectTimeLocationGroup}
|
||||
class=${group.key === selectedGroup ? "selected" : ""}
|
||||
>
|
||||
${icon}
|
||||
<div slot="headline">${group.name}</div>
|
||||
${
|
||||
narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-item-button>`;
|
||||
});
|
||||
|
||||
return html`<ha-section-title>${label}</ha-section-title> ${
|
||||
asTree
|
||||
? html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
|
||||
${groupItems} ${peopleItem}
|
||||
</wa-tree>`
|
||||
: html`<ha-list-base>
|
||||
${groupItems} ${narrow ? peopleItem : nothing}
|
||||
</ha-list-base>`
|
||||
}`;
|
||||
return html`<ha-section-title>${label}</ha-section-title>
|
||||
<ha-list-base>
|
||||
${groups.map(
|
||||
(group) =>
|
||||
html`<ha-list-item-button
|
||||
.value=${group.key}
|
||||
@click=${this._selectTimeLocationGroup}
|
||||
class=${group.key === selectedGroup ? "selected" : ""}
|
||||
>
|
||||
${
|
||||
group.icon
|
||||
? html`<span slot="start">${group.icon}</span>`
|
||||
: group.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${group.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
<div slot="headline">${group.name}</div>
|
||||
${
|
||||
narrow
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-item-button>`
|
||||
)}
|
||||
</ha-list-base>`;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -814,7 +761,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
return renderedDomains;
|
||||
}
|
||||
|
||||
private _renderEntities(entities: string[] = [], showTitle = true) {
|
||||
private _renderEntities(entities: string[] = []) {
|
||||
if (!entities.length) {
|
||||
return nothing;
|
||||
}
|
||||
@@ -852,15 +799,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
);
|
||||
|
||||
if (this.narrow) {
|
||||
return html`${
|
||||
showTitle
|
||||
? html`<ha-section-title
|
||||
>${this._i18n.localize(
|
||||
"ui.components.target-picker.type.entities"
|
||||
)}</ha-section-title
|
||||
>`
|
||||
: nothing
|
||||
} <ha-list-base>${renderedEntites}</ha-list-base>`;
|
||||
return html`<ha-section-title
|
||||
>${this._i18n.localize(
|
||||
"ui.components.target-picker.type.entities"
|
||||
)}</ha-section-title
|
||||
>
|
||||
<ha-list-base>${renderedEntites}</ha-list-base>`;
|
||||
}
|
||||
|
||||
return renderedEntites;
|
||||
@@ -1030,7 +974,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
this._loadUnassignedDevices();
|
||||
this._loadUnassignedEntities();
|
||||
this._loadPeople();
|
||||
this._entries = { ...this._entries };
|
||||
|
||||
if (this.value) {
|
||||
@@ -1038,15 +981,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _loadPeople() {
|
||||
const people = Object.keys(this._registries.entities).filter(
|
||||
(entityId) => computeDomain(entityId) === "person"
|
||||
);
|
||||
if (people.length) {
|
||||
this._entries[PERSON_TARGET] = { open: false, entities: people };
|
||||
}
|
||||
}
|
||||
|
||||
private _loadUnassignedDevices() {
|
||||
const unassignedDevices = Object.values(this._registries.devices).filter(
|
||||
(device) =>
|
||||
@@ -1326,13 +1260,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
}
|
||||
|
||||
if (type === "entity") {
|
||||
if (computeDomain(id) === "person" && this._entries[PERSON_TARGET]) {
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[PERSON_TARGET]: { ...this._entries[PERSON_TARGET], open: true },
|
||||
};
|
||||
}
|
||||
|
||||
const deviceId = this._registries.entities[id]?.device_id;
|
||||
if (deviceId && this._registries.devices[deviceId]) {
|
||||
this._setDeviceOpen(deviceId, true, true);
|
||||
@@ -1409,14 +1336,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
|
||||
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
|
||||
const treeItem = ev.detail.selection[0] as unknown as
|
||||
{ target?: string; value?: string } | undefined;
|
||||
|
||||
if (treeItem?.value) {
|
||||
fireEvent(this, "time-location-group-selected", {
|
||||
value: treeItem.value,
|
||||
});
|
||||
return;
|
||||
}
|
||||
{ target?: string } | undefined;
|
||||
|
||||
if (treeItem?.target) {
|
||||
this._valueChanged(treeItem.target);
|
||||
@@ -1467,14 +1387,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
private _toggleItem(targetId: string, open: boolean) {
|
||||
const [type, id] = targetId.split(TARGET_SEPARATOR, 2);
|
||||
|
||||
if (type === "person") {
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
[targetId]: { ...this._entries[targetId], open },
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "floor") {
|
||||
this._entries = {
|
||||
...this._entries,
|
||||
@@ -1659,7 +1571,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-list-item-button::part(headline) {
|
||||
ha-list-item-button::part(label) {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
font-family: var(--ha-font-family-heading);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export class HaSunCondition extends LitElement implements ConditionElement {
|
||||
name: "after_offset",
|
||||
selector: {
|
||||
duration: {
|
||||
allow_negative: true,
|
||||
mode: "offset",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,7 +96,7 @@ export class HaSunCondition extends LitElement implements ConditionElement {
|
||||
name: "before_offset",
|
||||
selector: {
|
||||
duration: {
|
||||
allow_negative: true,
|
||||
mode: "offset",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,16 +5,22 @@ import memoizeOne from "memoize-one";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { createDurationData } from "../../../common/datetime/create_duration_data";
|
||||
import { formatDurationNarrow } from "../../../common/datetime/format_duration";
|
||||
import { normalizeDuration } from "../../../common/datetime/normalize_duration";
|
||||
import { hasTemplate } from "../../../common/string/has-template";
|
||||
import type { ForDict } from "../../../data/automation";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { rowSummaryStyles } from "./styles";
|
||||
|
||||
const SUMMARY_UNITS = {
|
||||
enableDay: true,
|
||||
enableSecond: true,
|
||||
enableMillisecond: true,
|
||||
};
|
||||
|
||||
interface HaAutomationRowOptionsConfig {
|
||||
options?: {
|
||||
for?: string | number | ForDict;
|
||||
offset?: string | number | ForDict;
|
||||
offset_type?: "before" | "after";
|
||||
};
|
||||
timeout?: string | number | ForDict;
|
||||
}
|
||||
@@ -63,20 +69,18 @@ export class HaAutomationRowOptions extends LitElement {
|
||||
this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.row_options.for",
|
||||
{
|
||||
duration: forDuration,
|
||||
duration: forDuration.text,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const offsetDuration = this._duration(options.offset);
|
||||
if (offsetDuration) {
|
||||
const offsetType =
|
||||
options.offset_type === "before" ? "before" : "after";
|
||||
const offset = this._duration(options.offset);
|
||||
if (offset) {
|
||||
parts.push(
|
||||
this._i18n.localize(
|
||||
`ui.panel.config.automation.editor.row_options.offset_${offsetType}`,
|
||||
{ offset: offsetDuration }
|
||||
`ui.panel.config.automation.editor.row_options.offset_${offset.negative ? "before" : "after"}`,
|
||||
{ offset: offset.text }
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -88,7 +92,7 @@ export class HaAutomationRowOptions extends LitElement {
|
||||
parts.push(
|
||||
this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.row_options.timeout",
|
||||
{ duration: timeoutDuration }
|
||||
{ duration: timeoutDuration.text }
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -98,7 +102,9 @@ export class HaAutomationRowOptions extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _duration(value: unknown): string | undefined {
|
||||
private _duration(
|
||||
value: unknown
|
||||
): { negative: boolean; text: string } | undefined {
|
||||
if (value === undefined || hasTemplate(value)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -113,7 +119,9 @@ export class HaAutomationRowOptions extends LitElement {
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return formatDurationNarrow(this._i18n.locale, duration);
|
||||
const normalized = normalizeDuration(duration, SUMMARY_UNITS);
|
||||
const text = formatDurationNarrow(this._i18n.locale, normalized.duration);
|
||||
return text ? { negative: normalized.negative, text } : undefined;
|
||||
}
|
||||
|
||||
static styles = rowSummaryStyles;
|
||||
|
||||
@@ -708,6 +708,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
height: var(--header-height);
|
||||
padding: 4px;
|
||||
background-color: var(--primary-background-color);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
@@ -719,6 +720,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background-color: var(--card-background-color);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@@ -743,10 +745,7 @@ export class HaAutomationTrace extends LitElement {
|
||||
}
|
||||
|
||||
.graph {
|
||||
background: var(
|
||||
--ha-panel-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -773,13 +772,10 @@ export class HaAutomationTrace extends LitElement {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background: var(--ha-card-background, var(--card-background-color));
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
ha-tab-group {
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--ha-panel-background, var(--primary-background-color))
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { CalendarTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
import type { HaDurationData } from "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { durationDataToSeconds } from "../../../../../common/datetime/duration_to_seconds";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
|
||||
@@ -47,25 +44,10 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: "offset", required: true, selector: { duration: {} } },
|
||||
{
|
||||
name: "offset_type",
|
||||
type: "select",
|
||||
name: "offset",
|
||||
required: true,
|
||||
options: [
|
||||
[
|
||||
"before",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.before"
|
||||
),
|
||||
],
|
||||
[
|
||||
"after",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.after"
|
||||
),
|
||||
],
|
||||
],
|
||||
selector: { duration: { enable_day: true, mode: "offset" } },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
@@ -81,34 +63,10 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
protected render() {
|
||||
const schema = this._schema(this.hass.localize);
|
||||
// Convert from string representation to ha form duration representation
|
||||
const trigger_offset = this.trigger.offset;
|
||||
// Copy, `createDurationData` returns the input object as-is for dict values.
|
||||
const duration: HaDurationData = { ...createDurationData(trigger_offset)! };
|
||||
let offset_type = "after";
|
||||
if (durationDataToSeconds(duration) < 0) {
|
||||
// A negative offset negates the whole period, and the sign is shown by
|
||||
// the separate before/after select instead.
|
||||
if (duration.days) {
|
||||
duration.days = Math.abs(duration.days);
|
||||
}
|
||||
duration.hours = Math.abs(duration.hours ?? 0);
|
||||
duration.minutes = Math.abs(duration.minutes ?? 0);
|
||||
duration.seconds = Math.abs(duration.seconds ?? 0);
|
||||
if (duration.milliseconds) {
|
||||
duration.milliseconds = Math.abs(duration.milliseconds);
|
||||
}
|
||||
offset_type = "before";
|
||||
}
|
||||
const data = {
|
||||
...this.trigger,
|
||||
offset: duration,
|
||||
offset_type: offset_type,
|
||||
};
|
||||
return html`
|
||||
<ha-form
|
||||
.schema=${schema}
|
||||
.data=${data}
|
||||
.data=${this.trigger}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@@ -119,18 +77,7 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
// Convert back to duration string representation
|
||||
const duration = ev.detail.value.offset;
|
||||
const offsetType = ev.detail.value.offset_type === "before" ? "-" : "";
|
||||
const h = (duration.days ?? 0) * 24 + (duration.hours ?? 0);
|
||||
const m = duration.minutes ?? 0;
|
||||
const s = (duration.seconds ?? 0) + (duration.milliseconds ?? 0) / 1000;
|
||||
const newTrigger = {
|
||||
...ev.detail.value,
|
||||
offset: `${offsetType}${h}:${m}:${s}`,
|
||||
};
|
||||
delete newTrigger.offset_type;
|
||||
fireEvent(this, "value-changed", { value: newTrigger });
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
@@ -143,6 +90,10 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.event"
|
||||
);
|
||||
case "offset":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.calendar.offset"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaSunTrigger extends LitElement implements TriggerElement {
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: "offset", selector: { text: {} } },
|
||||
{ name: "offset", selector: { duration: { mode: "offset" } } },
|
||||
] as const
|
||||
);
|
||||
|
||||
|
||||
@@ -319,12 +319,12 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
protected async firstUpdated(changedProps: PropertyValues<this>) {
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._loadUsageCounts();
|
||||
if (this.route.path === "/import") {
|
||||
const url = extractSearchParam("blueprint_url");
|
||||
await navigate("/config/blueprint/dashboard", { replace: true });
|
||||
navigate("/config/blueprint/dashboard", { replace: true });
|
||||
if (url) {
|
||||
this._addBlueprint(url);
|
||||
}
|
||||
|
||||
@@ -578,8 +578,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._related.scene?.length
|
||||
? html`
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "automation") &&
|
||||
this._related.automation?.length
|
||||
isComponentLoaded(this.hass.config, "automation")
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
@@ -591,131 +590,156 @@ export class HaConfigDevicePage extends LitElement {
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(
|
||||
this._related
|
||||
).automation.map((automation) =>
|
||||
automation
|
||||
? html`<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(automation)}
|
||||
.href=${
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`
|
||||
${
|
||||
this._related.automation?.length
|
||||
? this._getRelated(
|
||||
this._related
|
||||
).automation.map((automation) =>
|
||||
automation
|
||||
? html`<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(automation)}
|
||||
.href=${
|
||||
automation.attributes.id
|
||||
? `/config/automation/edit/${encodeURIComponent(automation.attributes.id)}`
|
||||
: `/config/automation/show/${automation.entity_id}`
|
||||
}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
)
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
></ha-list-item-base>`
|
||||
}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "script")
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${
|
||||
this._related.script?.length
|
||||
? this._getRelated(
|
||||
this._related
|
||||
).script.map((script) => {
|
||||
if (!script) {
|
||||
return nothing;
|
||||
}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>`
|
||||
: nothing
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "script") &&
|
||||
this._related.script?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).script.map(
|
||||
(script) => {
|
||||
if (!script) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._entityReg.find(
|
||||
(e) => e.entity_id === script.entity_id
|
||||
);
|
||||
const url = entry
|
||||
? `/config/script/edit/${entry.unique_id}`
|
||||
: `/config/script/show/${script.entity_id}`;
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(script)}
|
||||
.href=${url}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasSceneSupport && this._related.scene?.length
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${this._getRelated(this._related).scene.map(
|
||||
(scene) => {
|
||||
if (!scene) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sceneId = `scene-${slugify(
|
||||
scene.entity_id
|
||||
)}`;
|
||||
|
||||
return scene.attributes.id
|
||||
? html`
|
||||
const entry = this._entityReg.find(
|
||||
(e) =>
|
||||
e.entity_id === script.entity_id
|
||||
);
|
||||
const url = entry
|
||||
? `/config/script/edit/${entry.unique_id}`
|
||||
: `/config/script/show/${script.entity_id}`;
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
.href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
.headline=${this.hass.formatEntityName(script)}
|
||||
.href=${url}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item-base
|
||||
id=${sceneId}
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-base>
|
||||
<ha-tooltip
|
||||
.for=${sceneId}
|
||||
placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
}
|
||||
})
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
></ha-list-item-base>`
|
||||
}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasSceneSupport
|
||||
? html`
|
||||
<h3 class="section-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list-nav
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
)}
|
||||
>
|
||||
${
|
||||
this._related.scene?.length
|
||||
? this._getRelated(
|
||||
this._related
|
||||
).scene.map((scene) => {
|
||||
if (!scene) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const sceneId = `scene-${slugify(
|
||||
scene.entity_id
|
||||
)}`;
|
||||
|
||||
return scene.attributes.id
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
.href=${`/config/scene/edit/${scene.attributes.id}`}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
: html`
|
||||
<ha-list-item-base
|
||||
id=${sceneId}
|
||||
.headline=${this.hass.formatEntityName(scene)}
|
||||
>
|
||||
<ha-icon-next
|
||||
slot="end"
|
||||
></ha-icon-next>
|
||||
</ha-list-item-base>
|
||||
<ha-tooltip
|
||||
.for=${sceneId}
|
||||
placement=${
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass
|
||||
.translationMetadata
|
||||
.translations
|
||||
)
|
||||
? "left"
|
||||
: "right"
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.cant_edit"
|
||||
)}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
})
|
||||
: html`<ha-list-item-base
|
||||
.headline=${this.hass.localize(
|
||||
"ui.panel.config.devices.scene.no_scenes"
|
||||
)}
|
||||
></ha-list-item-base>`
|
||||
}
|
||||
</ha-list-nav>
|
||||
`
|
||||
: nothing
|
||||
@@ -989,9 +1013,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${NAVIGATION_ACTIONS.filter(
|
||||
(action) => quickLinkCounts[action.countKey]
|
||||
).map(
|
||||
${NAVIGATION_ACTIONS.map(
|
||||
(action) => html`
|
||||
<ha-dropdown-item value=${action.value}>
|
||||
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
|
||||
@@ -1003,11 +1025,8 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
${
|
||||
NAVIGATION_ACTIONS.some((action) => quickLinkCounts[action.countKey])
|
||||
? html`<wa-divider></wa-divider>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item value="reset_entity_ids">
|
||||
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -25,14 +24,12 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import {
|
||||
devicesFilterQueryParamConfig,
|
||||
devicesFilterQueryParams,
|
||||
devicesLinkFromUrl,
|
||||
} from "../../../common/url/devices-query-params";
|
||||
getHistoryState,
|
||||
navigate,
|
||||
updateHistoryState,
|
||||
} from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -135,6 +132,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
private _selectedCanDelete: string[] = [];
|
||||
@@ -149,7 +148,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
private _filter: string = getHistoryState()?.filter || "";
|
||||
|
||||
@state()
|
||||
private _filters: DataTableFilters = {};
|
||||
@@ -197,16 +196,39 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
// The filters are kept in the URL by the page, the search, sorting and
|
||||
// grouping by the table
|
||||
private _filterParams = new QueryParamsController(
|
||||
this,
|
||||
devicesFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
private _ignoreLocationChange = false;
|
||||
|
||||
private _fromUrl = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
if (this._ignoreLocationChange) {
|
||||
this._ignoreLocationChange = false;
|
||||
return;
|
||||
}
|
||||
);
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _popState = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _states = memoizeOne((localize: LocalizeFunc) => [
|
||||
{
|
||||
@@ -219,7 +241,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._applyLink();
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
if (changedProps.has("_selected") || changedProps.has("entries")) {
|
||||
this._selectedCanDelete = this._selected.filter((d) => {
|
||||
@@ -234,38 +256,60 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Links, like the ones on an integration page, say in the URL what to show.
|
||||
// The router keeps the page open, so a link can land on it.
|
||||
private _applyLink() {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? devicesLinkFromUrl(searchParams) : undefined;
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const label = this._searchParms.has("label");
|
||||
|
||||
if (link) {
|
||||
// The filters only work out their items again when their value changes,
|
||||
// so keep the items of the ones the link leaves as they are
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(link.filters).map(([key, filter]) => {
|
||||
const current = this._filters[key];
|
||||
|
||||
return [
|
||||
key,
|
||||
current && deepEqual(filter.value, current.value)
|
||||
? current
|
||||
: filter,
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
this._filter = link.search;
|
||||
this._filters = filters;
|
||||
this._storageFilters = filters;
|
||||
if (!area && !domain && !configEntry && !label) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._writeFilters();
|
||||
this._fromUrl = true;
|
||||
this._filter = getHistoryState()?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...((this._filters["ha-filter-states"]?.value as string[]) || []),
|
||||
"disabled",
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-floor-areas": {
|
||||
value: area ? { areas: [area] } : undefined,
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: domain ? [domain] : [],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
sub_entry: {
|
||||
value: subEntry ? [subEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._filterLabel();
|
||||
}
|
||||
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(devicesFilterQueryParams(this._filters));
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-labels": {
|
||||
value: [label],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _reloadConfigEntries() {
|
||||
@@ -274,8 +318,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
}
|
||||
|
||||
private _devicesAndFilterDomains = memoizeOne(
|
||||
@@ -354,29 +399,24 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
Array.isArray(filter.value) &&
|
||||
filter.value.length
|
||||
) {
|
||||
const configEntryIds = this._filters.config_entry?.value;
|
||||
const subEntryIds = filter.value;
|
||||
|
||||
if (
|
||||
!Array.isArray(configEntryIds) ||
|
||||
!configEntryIds.length ||
|
||||
!Array.isArray(subEntryIds)
|
||||
) {
|
||||
if (!(
|
||||
Array.isArray(this._filters.config_entry?.value) &&
|
||||
this._filters.config_entry.value.length === 1
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
outputDevices = outputDevices.filter((device) =>
|
||||
configEntryIds.some((configEntryId) =>
|
||||
device.config_entries_subentries[configEntryId]?.some(
|
||||
(subEntryId) =>
|
||||
subEntryId !== null && subEntryIds.includes(subEntryId)
|
||||
const configEntryId = this._filters.config_entry.value[0];
|
||||
outputDevices = outputDevices.filter(
|
||||
(device) =>
|
||||
device.config_entries_subentries[configEntryId] &&
|
||||
(filter.value as string[]).some((subEntryId) =>
|
||||
device.config_entries_subentries[configEntryId].includes(
|
||||
subEntryId
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// The titles shown in the filter chip need a single config entry
|
||||
if (configEntryIds.length === 1 && !this._subEntries) {
|
||||
this._loadSubEntries(configEntryIds[0]);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(configEntryId);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
@@ -614,7 +654,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "integration",
|
||||
minWidth: "120px",
|
||||
template: (device) =>
|
||||
!this.entries
|
||||
@@ -630,7 +669,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "parent",
|
||||
defaultHidden: true,
|
||||
minWidth: "120px",
|
||||
template: (device) => device.parent_device_name || "",
|
||||
@@ -640,7 +678,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "manufacturer",
|
||||
minWidth: "120px",
|
||||
},
|
||||
model: {
|
||||
@@ -863,7 +900,6 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
sync-url
|
||||
has-fab
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
>
|
||||
@@ -1058,8 +1094,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
private _filterChanged(ev) {
|
||||
const type = ev.target.localName;
|
||||
this._filters = { ...this._filters, [type]: ev.detail };
|
||||
this._storageFilters = this._filters;
|
||||
this._writeFilters();
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
}
|
||||
}
|
||||
|
||||
private _batteryEntity(
|
||||
@@ -1086,11 +1123,13 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const deviceId = ev.detail.id;
|
||||
this._ignoreLocationChange = true;
|
||||
navigate(`/config/devices/device/${deviceId}`);
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
@@ -1118,13 +1157,8 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._filters["ha-filter-integrations"]?.value;
|
||||
showAddIntegrationDialog(this, {
|
||||
domain:
|
||||
Array.isArray(integrations) && integrations.length === 1
|
||||
? integrations[0]
|
||||
: undefined,
|
||||
domain: this._searchParms.get("domain") || undefined,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import "../../../helpers/forms/ha-input_datetime-form";
|
||||
import "../../../helpers/forms/ha-input_number-form";
|
||||
import "../../../helpers/forms/ha-input_select-form";
|
||||
import "../../../helpers/forms/ha-input_text-form";
|
||||
import "../../../helpers/forms/ha-schedule-form";
|
||||
import "../../../helpers/forms/ha-timer-form";
|
||||
import "../../entity-registry-settings-editor";
|
||||
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
|
||||
@@ -151,14 +152,7 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
}
|
||||
|
||||
private async _getItem() {
|
||||
const [items] = await Promise.all([
|
||||
HELPERS_CRUD[this.entry.platform].fetch(this.hass!),
|
||||
// Only schedules need FullCalendar, which some older browsers cannot
|
||||
// load. The form renders with the item, so it must be defined by then.
|
||||
this.entry.platform === "schedule"
|
||||
? import("../../../helpers/forms/ha-schedule-form")
|
||||
: undefined,
|
||||
]);
|
||||
const items = await HELPERS_CRUD[this.entry.platform].fetch(this.hass!);
|
||||
const item =
|
||||
items.find((helper) => helper.id === this.entry.unique_id) || null;
|
||||
this._item = item;
|
||||
|
||||
@@ -286,8 +286,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
|
||||
private _cloudStatusRequestId = 0;
|
||||
|
||||
private _cloudStatusTimeout?: number;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listeners.push(
|
||||
@@ -325,7 +323,6 @@ class HaPanelConfig extends HassRouterPage {
|
||||
}
|
||||
this._unsubCloudEvents?.then((unsub) => unsub()).catch(() => undefined);
|
||||
this._unsubCloudEvents = undefined;
|
||||
this._clearCloudStatusTimeout();
|
||||
entityRegistryByEntityId.clear();
|
||||
entityRegistryById.clear();
|
||||
}
|
||||
@@ -389,11 +386,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
}
|
||||
|
||||
private async _updateCloudStatus() {
|
||||
this._clearCloudStatusTimeout();
|
||||
const requestId = ++this._cloudStatusRequestId;
|
||||
const status = await fetchCloudStatus(this.hass);
|
||||
|
||||
if (requestId !== this._cloudStatusRequestId || !this.isConnected) {
|
||||
if (requestId !== this._cloudStatusRequestId) {
|
||||
return;
|
||||
}
|
||||
this._cloudStatus = status;
|
||||
@@ -406,17 +402,9 @@ class HaPanelConfig extends HassRouterPage {
|
||||
this._cloudStatus.prefs.remote_enabled &&
|
||||
!this._cloudStatus.remote_connected)
|
||||
) {
|
||||
this._cloudStatusTimeout = window.setTimeout(
|
||||
() => this._updateCloudStatus(),
|
||||
5000
|
||||
);
|
||||
setTimeout(() => this._updateCloudStatus(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private _clearCloudStatusTimeout() {
|
||||
clearTimeout(this._cloudStatusTimeout);
|
||||
this._cloudStatusTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DateSelectInfo,
|
||||
EventClickInfo,
|
||||
EventInput,
|
||||
} from "fullcalendar";
|
||||
import { Calendar } from "fullcalendar";
|
||||
import allLocales from "fullcalendar/locales-all";
|
||||
import interactionPlugin from "fullcalendar/interaction";
|
||||
import timeGridPlugin from "fullcalendar/timegrid";
|
||||
import type { CalendarOptions } from "@fullcalendar/core";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import type { Day } from "date-fns";
|
||||
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
@@ -22,16 +17,12 @@ import "../../../../components/input/ha-input";
|
||||
import type { Schedule, ScheduleDay } from "../../../../data/schedule";
|
||||
import { weekdays } from "../../../../data/schedule";
|
||||
import { TimeZone } from "../../../../data/translation";
|
||||
import {
|
||||
fullCalendarStyles,
|
||||
fullCalendarTheme,
|
||||
} from "../../../../resources/fullcalendar-theme";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
|
||||
|
||||
const defaultFullCalendarConfig: CalendarOptions = {
|
||||
plugins: [fullCalendarTheme, timeGridPlugin, interactionPlugin],
|
||||
plugins: [timeGridPlugin, interactionPlugin],
|
||||
headerToolbar: false,
|
||||
initialView: "timeGridWeek",
|
||||
editable: true,
|
||||
@@ -40,13 +31,10 @@ const defaultFullCalendarConfig: CalendarOptions = {
|
||||
selectOverlap: false,
|
||||
eventOverlap: false,
|
||||
allDaySlot: false,
|
||||
slotMinHeight: 24,
|
||||
height: 450,
|
||||
height: "parent",
|
||||
locales: allLocales,
|
||||
firstDay: 1,
|
||||
dayHeaderFormat: { weekday: "short", month: undefined, day: undefined },
|
||||
columnEventClass: "ha-fc-block",
|
||||
columnEventTimeClass: ({ isShort }) => (isShort ? "" : "ha-fc-block-time"),
|
||||
};
|
||||
|
||||
@customElement("ha-schedule-form")
|
||||
@@ -77,6 +65,8 @@ class HaScheduleForm extends LitElement {
|
||||
|
||||
@state() private calendar?: Calendar;
|
||||
|
||||
@query("style[data-fullcalendar]") private _fullCalendarStyle?: HTMLElement;
|
||||
|
||||
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
|
||||
|
||||
private _item?: Schedule;
|
||||
@@ -110,6 +100,7 @@ class HaScheduleForm extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
this.calendar?.destroy();
|
||||
this.calendar = undefined;
|
||||
this._fullCalendarStyle?.remove();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -197,7 +188,7 @@ class HaScheduleForm extends LitElement {
|
||||
...defaultFullCalendarConfig,
|
||||
locale: this.hass.language,
|
||||
firstDay: firstWeekdayIndex(this.hass.locale),
|
||||
slotHeaderFormat: {
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: undefined,
|
||||
hour12: useAmPm(this.hass.locale),
|
||||
@@ -225,7 +216,7 @@ class HaScheduleForm extends LitElement {
|
||||
}
|
||||
|
||||
private get _events() {
|
||||
const events: EventInput[] = [];
|
||||
const events: any[] = [];
|
||||
|
||||
for (const [i, day] of weekdays.entries()) {
|
||||
if (!this[`_${day}`].length) {
|
||||
@@ -265,7 +256,7 @@ class HaScheduleForm extends LitElement {
|
||||
return events;
|
||||
}
|
||||
|
||||
private _handleSelect(info: DateSelectInfo) {
|
||||
private _handleSelect(info: { start: Date; end: Date }) {
|
||||
const { start, end } = info;
|
||||
|
||||
const day = weekdays[start.getDay()];
|
||||
@@ -366,7 +357,7 @@ class HaScheduleForm extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleEventClick(info: EventClickInfo) {
|
||||
private async _handleEventClick(info: any) {
|
||||
const [day, index] = info.event.id.split("-");
|
||||
const item = [...this[`_${day}`]][index];
|
||||
showScheduleBlockInfoDialog(this, {
|
||||
@@ -429,15 +420,7 @@ class HaScheduleForm extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
fullCalendarStyles,
|
||||
css`
|
||||
:host {
|
||||
--fc-classic-background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@@ -449,16 +432,61 @@ class HaScheduleForm extends LitElement {
|
||||
|
||||
#calendar {
|
||||
margin: var(--ha-space-2) 0;
|
||||
height: 450px;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
--fc-border-color: var(--divider-color);
|
||||
--fc-event-border-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.ha-fc-block {
|
||||
border-color: var(--divider-color);
|
||||
.fc-v-event .fc-event-time {
|
||||
white-space: inherit;
|
||||
}
|
||||
.fc-theme-standard .fc-scrollgrid {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--mdc-shape-small, 4px);
|
||||
}
|
||||
|
||||
.ha-fc-block-time {
|
||||
white-space: normal;
|
||||
.fc-scrollgrid-section-header td {
|
||||
border: none;
|
||||
}
|
||||
:host([narrow]) .fc-scrollgrid-sync-table {
|
||||
overflow: hidden;
|
||||
}
|
||||
table.fc-scrollgrid-sync-table
|
||||
tbody
|
||||
tr:first-child
|
||||
.fc-daygrid-day-top {
|
||||
padding-top: 0;
|
||||
}
|
||||
.fc-scroller::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
.fc-scroller::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
.fc-scroller {
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.fc-timegrid-event-short .fc-event-time:after {
|
||||
content: ""; /* prevent trailing dash in half hour events since we do not have event titles */
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
th.fc-col-header-cell.fc-day {
|
||||
background-color: var(--table-header-background-color);
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -735,7 +735,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
!this._integrationFilter &&
|
||||
(PROTOCOL_INTEGRATIONS as readonly string[]).includes(
|
||||
integration.domain
|
||||
) &&
|
||||
@@ -793,10 +792,7 @@ class AddIntegrationDialog extends LitElement {
|
||||
}
|
||||
|
||||
private async _createFlow(domain: string) {
|
||||
// A filtered dialog sets up a given port, not a discovered device
|
||||
const flowsInProgress = this._integrationFilter
|
||||
? []
|
||||
: this._getFlowsInProgressForDomains([domain]);
|
||||
const flowsInProgress = this._getFlowsInProgressForDomains([domain]);
|
||||
|
||||
if (flowsInProgress.length) {
|
||||
this._pickedBrand = domain;
|
||||
|
||||
@@ -253,7 +253,7 @@ export class HaConfigEntryRow extends LitElement {
|
||||
href=${
|
||||
allDevices.length === 1
|
||||
? `/config/devices/device/${allDevices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="devices">
|
||||
@@ -278,7 +278,7 @@ export class HaConfigEntryRow extends LitElement {
|
||||
href=${
|
||||
allServices.length === 1
|
||||
? `/config/devices/device/${allServices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="services">
|
||||
|
||||
@@ -617,7 +617,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
href=${
|
||||
devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -635,7 +635,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
href=${
|
||||
services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&domain=${this.domain}`
|
||||
}
|
||||
>
|
||||
${this.hass.localize(
|
||||
|
||||
@@ -1109,10 +1109,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 16px;
|
||||
gap: var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -113,7 +113,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
href=${
|
||||
devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="devices">
|
||||
@@ -138,7 +138,7 @@ class HaConfigSubEntryRow extends LitElement {
|
||||
href=${
|
||||
services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}&status=disabled`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${configEntry.entry_id}&sub_entry=${subEntry.subentry_id}`
|
||||
}
|
||||
>
|
||||
<ha-dropdown-item value="services">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
domainToName,
|
||||
@@ -25,10 +25,6 @@ export class HaIntegrationActionCard extends LitElement {
|
||||
@property({ attribute: false }) public manifest?: IntegrationManifest;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const integrationName =
|
||||
this.localizedDomainName && this.localizedDomainName !== this.domain
|
||||
? this.localizedDomainName
|
||||
: domainToName(this.hass.localize, this.domain, this.manifest);
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
@@ -48,11 +44,14 @@ export class HaIntegrationActionCard extends LitElement {
|
||||
@load=${this._onImageLoad}
|
||||
/>
|
||||
<h2>${this.label}</h2>
|
||||
${
|
||||
integrationName !== this.label
|
||||
? html`<h3>${integrationName}</h3>`
|
||||
: nothing
|
||||
}
|
||||
<h3>
|
||||
${
|
||||
this.localizedDomainName &&
|
||||
this.localizedDomainName !== this.domain
|
||||
? this.localizedDomainName
|
||||
: domainToName(this.hass.localize, this.domain, this.manifest)
|
||||
}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="filler"></div>
|
||||
<div class="card-actions"><slot></slot></div>
|
||||
|
||||
@@ -178,7 +178,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -221,7 +221,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -237,7 +237,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export class DialogZWaveJSAddNode extends HTMLElement {
|
||||
|
||||
private async _openDialog() {
|
||||
await navigate(
|
||||
`/config/devices/dashboard?config_entry=${this.configEntryId}&status=disabled`,
|
||||
`/config/devices/dashboard?config_entry=${this.configEntryId}`,
|
||||
{
|
||||
replace: true,
|
||||
}
|
||||
|
||||
+16
-34
@@ -54,6 +54,7 @@ import {
|
||||
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { brandsUrl } from "../../../../../util/brands-url";
|
||||
@@ -62,7 +63,7 @@ import { showZWaveJSAddNodeDialog } from "./add-node/show-dialog-zwave_js-add-no
|
||||
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
|
||||
|
||||
@customElement("zwave_js-config-dashboard")
|
||||
class ZWaveJSConfigDashboard extends LitElement {
|
||||
class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
@@ -95,31 +96,13 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
|
||||
private _unsubscribeRestore?: UnsubscribeFunc;
|
||||
|
||||
@state() private _backupProgress?: number;
|
||||
private _backupProgress?: number;
|
||||
|
||||
@state() private _restoreProgress?: number;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hass && this.hasUpdated && !this._dialogOpen) {
|
||||
this._subscribeS2Inclusion();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeS2Inclusion();
|
||||
}
|
||||
private _restoreProgress?: number;
|
||||
|
||||
protected async firstUpdated() {
|
||||
if (this.hass) {
|
||||
if (this.isConnected) {
|
||||
this._subscribeS2Inclusion();
|
||||
}
|
||||
await this._fetchData();
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
if (this._status === "connected") {
|
||||
const inclusion_state = this._network?.controller.inclusion_state;
|
||||
// show dialog if inclusion/exclusion is already in progress
|
||||
@@ -132,6 +115,10 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [this._subscribeS2Inclusion()];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._configEntry) {
|
||||
return nothing;
|
||||
@@ -326,7 +313,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}&status=disabled`}
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
@@ -798,7 +785,11 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
|
||||
private _openInclusionDialog(dsk?: string, inclusionOngoing = false) {
|
||||
if (!this._dialogOpen) {
|
||||
this._unsubscribeS2Inclusion();
|
||||
// Unsubscribe from S2 inclusion before opening dialog
|
||||
if (this._s2InclusionUnsubscribe) {
|
||||
this._s2InclusionUnsubscribe.then((unsubscribe) => unsubscribe());
|
||||
this._s2InclusionUnsubscribe = undefined;
|
||||
}
|
||||
|
||||
showZWaveJSAddNodeDialog(this, {
|
||||
entry_id: this.configEntryId!,
|
||||
@@ -815,13 +806,10 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
// refresh the data after the dialog is closed. add a small delay for the inclusion state to update
|
||||
setTimeout(() => this._fetchData(), 100);
|
||||
this._dialogOpen = false;
|
||||
if (this.isConnected) {
|
||||
this._subscribeS2Inclusion();
|
||||
}
|
||||
this._subscribeS2Inclusion();
|
||||
};
|
||||
|
||||
private _subscribeS2Inclusion() {
|
||||
this._unsubscribeS2Inclusion();
|
||||
this._s2InclusionUnsubscribe = subscribeS2Inclusion(
|
||||
this.hass,
|
||||
this.configEntryId,
|
||||
@@ -829,13 +817,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
this._openInclusionDialog(message.dsk);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribeS2Inclusion() {
|
||||
this._s2InclusionUnsubscribe
|
||||
?.then((unsubscribe) => unsubscribe())
|
||||
.catch(() => undefined);
|
||||
this._s2InclusionUnsubscribe = undefined;
|
||||
return this._s2InclusionUnsubscribe;
|
||||
}
|
||||
|
||||
private _handleBackupMessage = (message: any) => {
|
||||
|
||||
@@ -478,7 +478,7 @@ export class HaConfigLabels extends LitElement {
|
||||
this._navigateConfig("/config/entities");
|
||||
break;
|
||||
case "navigate-devices":
|
||||
this._navigateConfig("/config/devices/dashboard", "&status=disabled");
|
||||
this._navigateConfig("/config/devices/dashboard");
|
||||
break;
|
||||
case "navigate-automations":
|
||||
this._navigateConfig("/config/automation/dashboard");
|
||||
@@ -495,10 +495,8 @@ export class HaConfigLabels extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _navigateConfig = (path: string, params = "") => {
|
||||
navigate(
|
||||
`${path}?historyBack=1&label=${this._overflowLabel.label_id}${params}`
|
||||
);
|
||||
private _navigateConfig = (path: string) => {
|
||||
navigate(`${path}?historyBack=1&label=${this._overflowLabel.label_id}`);
|
||||
};
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import { internationalizationContext } from "../../../data/context";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import "./error-log-card";
|
||||
import type { LogsDialogParams } from "./show-dialog-logs";
|
||||
|
||||
@customElement("dialog-logs")
|
||||
class LogsDialog extends DialogMixin<LogsDialogParams>(LitElement) {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
// The log card's strings live in the config fragment, which isn't loaded on dashboards.
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog open header-title=${this.params.title} width="large">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
.placeholder=${this._i18n.localize("ui.panel.config.logs.search")}
|
||||
@input=${this._filterChanged}
|
||||
></ha-input-search>
|
||||
<error-log-card
|
||||
.header=${this.params.title}
|
||||
.provider=${this.params.provider}
|
||||
.filter=${this._filter}
|
||||
></error-log-card>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: InputEvent) {
|
||||
this._filter = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-input-search {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
error-log-card {
|
||||
--error-log-card-height: calc(100vh - 340px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-logs": LogsDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface LogsDialogParams {
|
||||
provider: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const showLogsDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: LogsDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-logs",
|
||||
dialogImport: () => import("./dialog-logs"),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -681,6 +681,7 @@ export class HaScriptTrace extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--header-height);
|
||||
background-color: var(--primary-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
@@ -691,6 +692,7 @@ export class HaScriptTrace extends LitElement {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
:host([narrow]) .main {
|
||||
@@ -714,10 +716,7 @@ export class HaScriptTrace extends LitElement {
|
||||
}
|
||||
|
||||
.graph {
|
||||
background: var(
|
||||
--ha-panel-background,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -744,13 +743,10 @@ export class HaScriptTrace extends LitElement {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background: var(--ha-card-background, var(--card-background-color));
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
ha-tab-group {
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--ha-panel-background, var(--primary-background-color))
|
||||
);
|
||||
background-color: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,6 @@ export class DialogDebugConsoleWarning extends DialogMixin<DebugConsoleWarningDi
|
||||
</ha-alert>
|
||||
<ha-checkbox
|
||||
autofocus
|
||||
variant="warning"
|
||||
.checked=${this._understood}
|
||||
.disabled=${this._accepting}
|
||||
@change=${this._understoodChanged}
|
||||
@@ -140,7 +139,6 @@ export class DialogDebugConsoleWarning extends DialogMixin<DebugConsoleWarningDi
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
variant="warning"
|
||||
.disabled=${!this._understood || this._secondsLeft > 0}
|
||||
.loading=${this._accepting}
|
||||
@click=${this._accept}
|
||||
|
||||
@@ -912,13 +912,10 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
gap: var(--ha-space-4);
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
background: var(
|
||||
--ha-panel-toolbar-background,
|
||||
var(--ha-panel-background, var(--primary-background-color))
|
||||
);
|
||||
}
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
|
||||
@@ -266,6 +266,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: var(--header-height);
|
||||
background-color: var(--primary-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -13,14 +13,10 @@ import type {
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
@@ -49,8 +45,6 @@ import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-empty-state";
|
||||
import "../../components/ha-filter-pane";
|
||||
import type { HaFilterPane } from "../../components/ha-filter-pane";
|
||||
import "../../components/ha-filter-entity-types";
|
||||
import "../../components/ha-filter-integrations";
|
||||
import "../../components/ha-filter-pane-chip";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { SourceFilters } from "../../components/ha-sources-picker";
|
||||
@@ -58,8 +52,6 @@ import {
|
||||
applySourceFilters,
|
||||
countSourceFilters,
|
||||
countTargets,
|
||||
setSourceFilter,
|
||||
sourceFiltersStyles,
|
||||
} from "../../components/ha-sources-picker";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
@@ -113,10 +105,6 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
@state() private _filters: SourceFilters = {};
|
||||
|
||||
// Only one filter is expanded at a time, so that it can use the height
|
||||
// that is left in the pane.
|
||||
@state() private _expandedFilter?: string;
|
||||
|
||||
@storage({
|
||||
key: "historySourceFilters",
|
||||
state: false,
|
||||
@@ -247,30 +235,8 @@ class HaPanelHistory extends LitElement {
|
||||
"ui.panel.history.no_targets"
|
||||
)}
|
||||
@value-changed=${this._targetsChanged}
|
||||
@source-filters-changed=${this._filtersChanged}
|
||||
></ha-sources-picker>
|
||||
<div
|
||||
class=${classMap({
|
||||
filters: true,
|
||||
expanded: !!this._expandedFilter,
|
||||
})}
|
||||
>
|
||||
<ha-filter-entity-types
|
||||
.value=${this._filters.types}
|
||||
.expanded=${
|
||||
this._expandedFilter === "ha-filter-entity-types"
|
||||
}
|
||||
@data-table-filter-changed=${this._typesChanged}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-entity-types>
|
||||
<ha-filter-integrations
|
||||
.value=${this._filters.integrations}
|
||||
.expanded=${
|
||||
this._expandedFilter === "ha-filter-integrations"
|
||||
}
|
||||
@data-table-filter-changed=${this._integrationsChanged}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
</div>
|
||||
</ha-filter-pane>`
|
||||
: nothing
|
||||
}
|
||||
@@ -402,8 +368,6 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
// The filters label themselves with keys from the config panel.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
@@ -452,37 +416,13 @@ class HaPanelHistory extends LitElement {
|
||||
this._showSources = false;
|
||||
}
|
||||
|
||||
private _typesChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
private _filtersChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["source-filters-changed"]>
|
||||
) {
|
||||
this._setFilter("types", ev);
|
||||
}
|
||||
|
||||
private _integrationsChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
) {
|
||||
this._setFilter("integrations", ev);
|
||||
}
|
||||
|
||||
private _setFilter(
|
||||
key: keyof SourceFilters,
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
) {
|
||||
this._filters = setSourceFilter(this._filters, key, ev.detail.value);
|
||||
this._filters = ev.detail.value;
|
||||
this._storedFilters = this._filters;
|
||||
}
|
||||
|
||||
private _filterExpanded(
|
||||
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]> &
|
||||
HASSDomTargetEvent<HTMLElement>
|
||||
) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = ev.target.localName;
|
||||
} else if (this._expandedFilter === ev.target.localName) {
|
||||
this._expandedFilter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearSources() {
|
||||
this._filters = {};
|
||||
this._storedFilters = this._filters;
|
||||
@@ -857,14 +797,12 @@ class HaPanelHistory extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
sourceFiltersStyles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
/* The target picker chips need more room than a plain filter list. */
|
||||
--ha-filter-pane-width: 340px;
|
||||
}
|
||||
|
||||
ha-top-app-bar-fixed {
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
@@ -916,6 +854,7 @@ class HaPanelHistory extends LitElement {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { throttle } from "../../common/util/throttle";
|
||||
@@ -15,7 +14,6 @@ import { fetchUsers } from "../../data/user";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-logbook-renderer";
|
||||
import type { LogbookNameDetail } from "./logbook-entry-model";
|
||||
import { computeLogbookCauseKey, computeRowCause } from "./logbook-entry-model";
|
||||
|
||||
interface LogbookTimePeriod {
|
||||
now: Date;
|
||||
@@ -59,9 +57,6 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateFilter?: string[];
|
||||
|
||||
/** Keys from `computeLogbookCauseKey` to keep. */
|
||||
@property({ attribute: false }) public causeFilter?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public virtualize = false;
|
||||
@@ -86,12 +81,10 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
@state() private _traceContexts: TraceContexts = {};
|
||||
|
||||
@state() private _userIdToName: Record<string, string> = {};
|
||||
@state() private _userIdToName = {};
|
||||
|
||||
@state() private _systemUserIds = new Set<string>();
|
||||
|
||||
@state() private _personUserIds = new Set<string>();
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsubLogbook?: Promise<UnsubscribeFunc>;
|
||||
@@ -110,7 +103,7 @@ export class HaLogbook extends LitElement {
|
||||
private _readyListenerAttached = false;
|
||||
|
||||
public getEntries(): LogbookEntry[] {
|
||||
return this._shownEntries() ?? [];
|
||||
return this._logbookEntries || [];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -126,9 +119,7 @@ export class HaLogbook extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const entries = this._shownEntries();
|
||||
|
||||
if (entries === undefined) {
|
||||
if (this._logbookEntries === undefined) {
|
||||
return html`
|
||||
<div class="progress-wrapper">
|
||||
<ha-spinner></ha-spinner>
|
||||
@@ -136,7 +127,7 @@ export class HaLogbook extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
if (this._logbookEntries.length === 0) {
|
||||
return html`<slot name="empty">
|
||||
<div class="no-entries">
|
||||
${this.hass.localize("ui.components.logbook.entries_not_found")}
|
||||
@@ -153,7 +144,7 @@ export class HaLogbook extends LitElement {
|
||||
.graphColor=${this.graphColor}
|
||||
.showCause=${this.showCause}
|
||||
.nameDetail=${this.nameDetail}
|
||||
.entries=${entries}
|
||||
.entries=${this._logbookEntries}
|
||||
.traceContexts=${this._traceContexts}
|
||||
.userIdToName=${this._userIdToName}
|
||||
.systemUserIds=${this._systemUserIds}
|
||||
@@ -162,40 +153,6 @@ export class HaLogbook extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _shownEntries(): LogbookEntry[] | undefined {
|
||||
return this._filterEntriesByCause(
|
||||
this._logbookEntries,
|
||||
this.causeFilter,
|
||||
this._userIdToName,
|
||||
this._systemUserIds,
|
||||
this._personUserIds
|
||||
);
|
||||
}
|
||||
|
||||
private _filterEntriesByCause = memoizeOne(
|
||||
(
|
||||
entries: LogbookEntry[] | undefined,
|
||||
causeFilter: string[] | undefined,
|
||||
userIdToName: Record<string, string>,
|
||||
systemUserIds: Set<string>,
|
||||
personUserIds: Set<string>
|
||||
): LogbookEntry[] | undefined => {
|
||||
if (!entries || !causeFilter?.length) {
|
||||
return entries;
|
||||
}
|
||||
const causes = new Set(causeFilter);
|
||||
return entries.filter((entry) =>
|
||||
causes.has(
|
||||
computeLogbookCauseKey(
|
||||
entry,
|
||||
computeRowCause(this.hass, entry, userIdToName, systemUserIds),
|
||||
personUserIds
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
public async refresh(force = false) {
|
||||
if (!force && (this._unsubLogbook || this._logbookEntries === undefined)) {
|
||||
return;
|
||||
@@ -247,26 +204,18 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
if (changed) {
|
||||
this.refresh(true);
|
||||
} else if (changedProps.has("causeFilter")) {
|
||||
// A filter without matches removes the renderer, which is what resumes
|
||||
// the live updates paused while scrolled down.
|
||||
this._setLiveUpdates(true);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLogbookLive(ev: CustomEvent) {
|
||||
this._setLiveUpdates(ev.detail.enable);
|
||||
}
|
||||
|
||||
private _setLiveUpdates(enable: boolean) {
|
||||
if (enable && !this._liveUpdatesEnabled) {
|
||||
if (ev.detail.enable && !this._liveUpdatesEnabled) {
|
||||
// Process everything we queued up while we were scrolled down
|
||||
this._pendingStreamMessages.forEach((msg) =>
|
||||
this._processStreamMessage(msg)
|
||||
);
|
||||
this._pendingStreamMessages = [];
|
||||
}
|
||||
this._liveUpdatesEnabled = enable;
|
||||
this._liveUpdatesEnabled = ev.detail.enable;
|
||||
}
|
||||
|
||||
private get _filterAlwaysEmptyResults(): boolean {
|
||||
@@ -522,7 +471,6 @@ export class HaLogbook extends LitElement {
|
||||
private _updateUsers = throttle(async () => {
|
||||
const userIdToName = {};
|
||||
const systemUserIds = new Set<string>();
|
||||
const personUserIds = new Set<string>();
|
||||
|
||||
// Start loading users
|
||||
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
|
||||
@@ -535,7 +483,6 @@ export class HaLogbook extends LitElement {
|
||||
) {
|
||||
userIdToName[entity.attributes.user_id] =
|
||||
this.hass.formatEntityName(entity);
|
||||
personUserIds.add(entity.attributes.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,7 +501,6 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
this._userIdToName = userIdToName;
|
||||
this._systemUserIds = systemUserIds;
|
||||
this._personUserIds = personUserIds;
|
||||
}, 60000);
|
||||
|
||||
static get styles() {
|
||||
|
||||
@@ -9,15 +9,11 @@ import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fromUnixTime } from "date-fns";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
@@ -38,9 +34,6 @@ import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-empty-state";
|
||||
import "../../components/ha-filter-entity-types";
|
||||
import "../../components/ha-filter-integrations";
|
||||
import "../../components/ha-filter-logbook-causes";
|
||||
import "../../components/ha-filter-pane-chip";
|
||||
import "../../components/ha-filter-pane";
|
||||
import type { HaFilterPane } from "../../components/ha-filter-pane";
|
||||
@@ -48,8 +41,6 @@ import "../../components/ha-icon-button";
|
||||
import {
|
||||
applySourceFilters,
|
||||
countSourceFilters,
|
||||
setSourceFilter,
|
||||
sourceFiltersStyles,
|
||||
countTargets,
|
||||
} from "../../components/ha-sources-picker";
|
||||
import type { SourceFilters } from "../../components/ha-sources-picker";
|
||||
@@ -68,11 +59,6 @@ import { csvDownload, csvSafeString } from "../../util/csv";
|
||||
|
||||
const EMPTY_STATES: HomeAssistant["states"] = {};
|
||||
|
||||
interface LogbookFilters extends SourceFilters {
|
||||
/** Keys from `computeLogbookCauseKey`. */
|
||||
causes?: string[];
|
||||
}
|
||||
|
||||
interface LogbookState {
|
||||
time: { range: [Date, Date] };
|
||||
targetPickerValue: HassServiceTarget;
|
||||
@@ -91,11 +77,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
@state()
|
||||
private _showBack?: boolean;
|
||||
|
||||
@state() private _filters: LogbookFilters = {};
|
||||
|
||||
// Only one filter is expanded at a time, so that it can use the height
|
||||
// that is left in the pane.
|
||||
@state() private _expandedFilter?: string;
|
||||
@state() private _filters: SourceFilters = {};
|
||||
|
||||
@state() private _showSources?: boolean;
|
||||
|
||||
@@ -120,7 +102,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _storedFilters?: LogbookFilters;
|
||||
private _storedFilters?: SourceFilters;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
@@ -131,7 +113,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
const entityIds = this._getEntityIds();
|
||||
const filterCount = countSourceFilters(this._filters);
|
||||
const sourceCount = countTargets(this._targetPickerValue) + filterCount;
|
||||
const sourcesLabel = entityIds
|
||||
const sourcesLabel = sourceCount
|
||||
? this.hass.localize("ui.panel.logbook.sources_count", {
|
||||
count: entityIds?.length ?? 0,
|
||||
})
|
||||
@@ -185,38 +167,8 @@ export class HaPanelLogbook extends LitElement {
|
||||
"ui.panel.logbook.no_targets"
|
||||
)}
|
||||
@value-changed=${this._targetsChanged}
|
||||
@source-filters-changed=${this._filtersChanged}
|
||||
></ha-sources-picker>
|
||||
<div
|
||||
class=${classMap({
|
||||
filters: true,
|
||||
expanded: !!this._expandedFilter,
|
||||
})}
|
||||
>
|
||||
<ha-filter-entity-types
|
||||
.value=${this._filters.types}
|
||||
.expanded=${
|
||||
this._expandedFilter === "ha-filter-entity-types"
|
||||
}
|
||||
@data-table-filter-changed=${this._typesChanged}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-entity-types>
|
||||
<ha-filter-integrations
|
||||
.value=${this._filters.integrations}
|
||||
.expanded=${
|
||||
this._expandedFilter === "ha-filter-integrations"
|
||||
}
|
||||
@data-table-filter-changed=${this._integrationsChanged}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-integrations>
|
||||
<ha-filter-logbook-causes
|
||||
.value=${this._filters.causes}
|
||||
.expanded=${
|
||||
this._expandedFilter === "ha-filter-logbook-causes"
|
||||
}
|
||||
@data-table-filter-changed=${this._causesChanged}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-logbook-causes>
|
||||
</div>
|
||||
</ha-filter-pane>`
|
||||
: nothing
|
||||
}
|
||||
@@ -246,7 +198,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.time=${this._time}
|
||||
.entityIds=${this._shownEntityIds}
|
||||
.causeFilter=${this._shownCauses}
|
||||
.narrow=${this.narrow}
|
||||
show-cause
|
||||
virtualize
|
||||
@@ -302,43 +253,13 @@ export class HaPanelLogbook extends LitElement {
|
||||
this._showSources = false;
|
||||
}
|
||||
|
||||
private _typesChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
private _filtersChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["source-filters-changed"]>
|
||||
) {
|
||||
this._setFilter("types", ev);
|
||||
}
|
||||
|
||||
private _integrationsChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
) {
|
||||
this._setFilter("integrations", ev);
|
||||
}
|
||||
|
||||
private _causesChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
) {
|
||||
this._setFilter("causes", ev);
|
||||
}
|
||||
|
||||
private _setFilter(
|
||||
key: keyof LogbookFilters,
|
||||
ev: HASSDomEvent<HASSDomEvents["data-table-filter-changed"]>
|
||||
) {
|
||||
this._filters = setSourceFilter(this._filters, key, ev.detail.value);
|
||||
this._filters = ev.detail.value;
|
||||
this._storedFilters = this._filters;
|
||||
}
|
||||
|
||||
private _filterExpanded(
|
||||
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]> &
|
||||
HASSDomTargetEvent<HTMLElement>
|
||||
) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = ev.target.localName;
|
||||
} else if (this._expandedFilter === ev.target.localName) {
|
||||
this._expandedFilter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _clearSources() {
|
||||
this._filters = {};
|
||||
this._storedFilters = this._filters;
|
||||
@@ -361,15 +282,12 @@ export class HaPanelLogbook extends LitElement {
|
||||
// the sources once it is closed instead of on every source change.
|
||||
if (!this.narrow || !this._sourcesShown()) {
|
||||
this._shownEntityIds = this._getEntityIds();
|
||||
this._shownCauses = this._filters.causes;
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
// The filters label themselves with keys from the config panel.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
@@ -414,7 +332,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!this._filters.types?.length && !this._filters.integrations?.length) {
|
||||
if (!countSourceFilters(this._filters)) {
|
||||
return targetEntities;
|
||||
}
|
||||
|
||||
@@ -480,8 +398,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
|
||||
private _shownEntityIds?: string[];
|
||||
|
||||
private _shownCauses?: string[];
|
||||
|
||||
// A list keyed on the states must keep its identity or ha-logbook resubscribes.
|
||||
private _stableEntityIds(entityIds: string[]): string[] {
|
||||
if (this._lastEntityIds && shallowEqual(this._lastEntityIds, entityIds)) {
|
||||
@@ -664,7 +580,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
sourceFiltersStyles,
|
||||
css`
|
||||
:host {
|
||||
--ha-generic-picker-width: min(400px, calc(100vw - 32px));
|
||||
@@ -707,6 +622,7 @@ export class HaPanelLogbook extends LitElement {
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
direction: var(--direction);
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { domainToName } from "../../data/integration";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
createHistoricState,
|
||||
logbookUserCauseKey,
|
||||
localizeStateMessage,
|
||||
parseTriggerSource,
|
||||
} from "../../data/logbook";
|
||||
@@ -306,46 +305,6 @@ export const computeLogbookCause = (
|
||||
export const isRunCause = (cause?: LogbookCause): boolean =>
|
||||
cause?.type === "automation" || cause?.type === "script";
|
||||
|
||||
// A row shows the run over the user who started it; the dialog shows both.
|
||||
export const computeRowCause = (
|
||||
hass: HomeAssistant,
|
||||
item: LogbookEntry,
|
||||
userIdToName: Record<string, string>,
|
||||
systemUserIds?: Set<string>
|
||||
): LogbookCause | undefined => {
|
||||
const contextCause = computeContextCause(hass, item);
|
||||
if (isRunCause(contextCause)) {
|
||||
return contextCause;
|
||||
}
|
||||
return computeUserCause(item, userIdToName, systemUserIds) ?? contextCause;
|
||||
};
|
||||
|
||||
// Only the users linked to a person can be picked. The other users are
|
||||
// accounts for tools like Node-RED, so they count as integrations. A service
|
||||
// call without a user carries the domain of the service, not the integration
|
||||
// that acted, so it is left in other.
|
||||
export const computeLogbookCauseKey = (
|
||||
item: LogbookEntry,
|
||||
cause: LogbookCause | undefined,
|
||||
personUserIds: Set<string>
|
||||
): string => {
|
||||
if (cause?.type === "user") {
|
||||
return personUserIds.has(cause.userId!)
|
||||
? logbookUserCauseKey(cause.userId!)
|
||||
: "integration";
|
||||
}
|
||||
if (cause?.type === "automation" || cause?.type === "script") {
|
||||
return cause.type;
|
||||
}
|
||||
if (
|
||||
cause?.type === "integration" &&
|
||||
item.context_event_type !== "call_service"
|
||||
) {
|
||||
return "integration";
|
||||
}
|
||||
return "other";
|
||||
};
|
||||
|
||||
export type LogbookGlyph =
|
||||
| { type: "state"; stateObj: HassEntity; icon?: string }
|
||||
| { type: "automation"; script: boolean }
|
||||
@@ -453,6 +412,13 @@ export const computeLogbookItem = (
|
||||
? entityDisplay(hass, entry.entity_id, opts.nameDetail)
|
||||
: undefined;
|
||||
|
||||
const userCause = computeUserCause(
|
||||
entry,
|
||||
opts.userIdToName ?? {},
|
||||
opts.systemUserIds
|
||||
);
|
||||
const contextCause = computeContextCause(hass, entry);
|
||||
|
||||
return {
|
||||
category,
|
||||
glyph: computeLogbookGlyph(entry, category, historicStateObj, domain),
|
||||
@@ -460,12 +426,10 @@ export const computeLogbookItem = (
|
||||
name: display?.primary ?? entry.name,
|
||||
context: display?.secondary,
|
||||
value: computeLogbookValue(hass, entry, domain, historicStateObj),
|
||||
cause: computeRowCause(
|
||||
hass,
|
||||
entry,
|
||||
opts.userIdToName ?? {},
|
||||
opts.systemUserIds
|
||||
),
|
||||
// A row shows the run over the user who started it; the dialog shows both.
|
||||
cause: isRunCause(contextCause)
|
||||
? contextCause
|
||||
: (userCause ?? contextCause),
|
||||
when: entry.when * 1000,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -55,8 +55,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
|
||||
@state() private _date?: string;
|
||||
|
||||
@queryAll(".hand") private _hands!: NodeListOf<HTMLElement>;
|
||||
|
||||
private _dateInterval?: number;
|
||||
|
||||
private _timeZone?: string;
|
||||
@@ -89,27 +87,6 @@ export class HuiClockCardAnalog extends LitElement {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("_hourOffsetSec") ||
|
||||
changedProps.has("_minuteOffsetSec") ||
|
||||
changedProps.has("_secondOffsetSec")
|
||||
) {
|
||||
this._restartHandAnimations();
|
||||
}
|
||||
}
|
||||
|
||||
// The hands are positioned by a negative animation-delay computed from the
|
||||
// current time. Changing the delay of a running animation keeps its original
|
||||
// start time, so the time elapsed since then would be counted twice and the
|
||||
// clock would run ahead. Restart the animations so the new delay applies
|
||||
// from now.
|
||||
private _restartHandAnimations() {
|
||||
this._hands.forEach((hand) => {
|
||||
hand.getAnimations().forEach((animation) => {
|
||||
animation.currentTime = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
|
||||
@@ -974,7 +974,7 @@ export class HuiEnergySourcesTableCard
|
||||
text-transform: inherit;
|
||||
}
|
||||
.mdc-data-table {
|
||||
background: transparent;
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border: 0;
|
||||
box-sizing: border-box;
|
||||
@@ -1022,7 +1022,7 @@ export class HuiEnergySourcesTableCard
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mdc-data-table__header-cell {
|
||||
background: transparent;
|
||||
background-color: var(--card-background-color);
|
||||
font-family: var(--ha-font-family-body);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
|
||||
@@ -375,7 +375,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
up-to-now
|
||||
.hoursToShow=${this._hoursToShow}
|
||||
.showNames=${showNames}
|
||||
?inside-labels=${this._config.label_position === "below"}
|
||||
.logarithmicScale=${this._config.logarithmic_scale || false}
|
||||
.minYAxis=${this._config.min_y_axis}
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
|
||||
@@ -507,7 +507,6 @@ export interface HistoryGraphCardConfig extends LovelaceCardConfig {
|
||||
hours_to_show?: number;
|
||||
title?: string;
|
||||
show_names?: boolean;
|
||||
label_position?: "side" | "below";
|
||||
logarithmic_scale?: boolean;
|
||||
min_y_axis?: number;
|
||||
max_y_axis?: number;
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
import { downSampleLineData } from "../../../../components/chart/down-sample";
|
||||
import type { EntityHistoryState } from "../../../../data/history";
|
||||
|
||||
export interface GraphSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const scaleCoordinates = (
|
||||
coordinates: number[][],
|
||||
from: GraphSize,
|
||||
to: GraphSize
|
||||
): number[][] => {
|
||||
if (from.width === to.width && from.height === to.height) {
|
||||
return coordinates;
|
||||
}
|
||||
const scaleX = to.width / from.width;
|
||||
const scaleY = to.height / from.height;
|
||||
return coordinates.map(([x, y]) => [x * scaleX, y * scaleY]);
|
||||
};
|
||||
|
||||
const calcPoints = (
|
||||
history: [number, number][],
|
||||
width: number,
|
||||
|
||||
@@ -5,8 +5,6 @@ import { consume } from "../../../common/decorators/consume";
|
||||
import { parseAnimationDuration } from "../../../common/util/parse-animation-duration";
|
||||
import { reducedMotionContext } from "../../../data/context";
|
||||
import { strokeWidth } from "../../../data/graph";
|
||||
import type { GraphSize } from "../common/graph/coordinates";
|
||||
import { scaleCoordinates } from "../common/graph/coordinates";
|
||||
import { getPath } from "../common/graph/get-path";
|
||||
|
||||
export interface HuiGraphGradient {
|
||||
@@ -39,13 +37,9 @@ export class HuiGraphBase extends LitElement {
|
||||
|
||||
private _animationFrame?: number;
|
||||
|
||||
// The size the current coordinates were computed for. The viewBox keeps it
|
||||
// until new coordinates arrive, so a resize during the animation scales the
|
||||
// graph instead of drawing it in a viewBox of another size.
|
||||
private _size?: GraphSize;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { width, height } = this._size ?? this._measureSize();
|
||||
const width = this.clientWidth || 500;
|
||||
const height = this.clientHeight || width / 5;
|
||||
const yAxisOrigin = this.yAxisOrigin ?? height;
|
||||
const path =
|
||||
(this._displayCoordinates && getPath(this._displayCoordinates)) ??
|
||||
@@ -141,25 +135,9 @@ export class HuiGraphBase extends LitElement {
|
||||
this._cancelAnimation();
|
||||
}
|
||||
|
||||
private _measureSize(): GraphSize {
|
||||
const width = this.clientWidth || 500;
|
||||
return { width, height: this.clientHeight || width / 5 };
|
||||
}
|
||||
|
||||
private _setCoordinates(coordinates: number[][]) {
|
||||
this._cancelAnimation();
|
||||
|
||||
const previousSize = this._size;
|
||||
this._size = this._measureSize();
|
||||
|
||||
if (this._displayCoordinates && previousSize) {
|
||||
this._displayCoordinates = scaleCoordinates(
|
||||
this._displayCoordinates,
|
||||
previousSize,
|
||||
this._size
|
||||
);
|
||||
}
|
||||
|
||||
const displayCoordinates = this._displayCoordinates;
|
||||
|
||||
if (!displayCoordinates || coordinates.length < 2) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
@@ -24,7 +23,6 @@ import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { isTimelineEntity } from "../../../../data/history";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-history-graph-card";
|
||||
import type {
|
||||
@@ -48,7 +46,6 @@ const cardConfigStruct = assign(
|
||||
hours_to_show: optional(number()),
|
||||
refresh_interval: optional(number()), // deprecated
|
||||
show_names: optional(boolean()),
|
||||
label_position: optional(enums(["side", "below"])),
|
||||
logarithmic_scale: optional(boolean()),
|
||||
expand_legend: optional(boolean()),
|
||||
min_y_axis: optional(number()),
|
||||
@@ -57,6 +54,65 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 0, step: "any", mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "show_names",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "logarithmic_scale",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "min_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "max_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
visible: {
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "min_y_axis", operator: "exists" },
|
||||
{ field: "max_y_axis", operator: "exists" },
|
||||
],
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
@customElement("hui-history-graph-card-editor")
|
||||
export class HuiHistoryGraphCardEditor
|
||||
extends LitElement
|
||||
@@ -68,90 +124,6 @@ export class HuiHistoryGraphCardEditor
|
||||
|
||||
@state() private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, hasTimeline: boolean) =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 0, step: "any", mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "show_names",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "logarithmic_scale",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
...(hasTimeline
|
||||
? ([
|
||||
{
|
||||
name: "label_position",
|
||||
required: false,
|
||||
visible: [
|
||||
{ field: "show_names", operator: "not_eq", value: false },
|
||||
],
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["side", "below"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.history-graph.label_position_options.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "min_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "max_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
visible: {
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "min_y_axis", operator: "exists" },
|
||||
{ field: "max_y_axis", operator: "exists" },
|
||||
],
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
public setConfig(config: HistoryGraphCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
@@ -207,19 +179,12 @@ export class HuiHistoryGraphCardEditor
|
||||
const configEntities = this._config.entities
|
||||
? (processEditorEntities(this._config.entities) as GraphEntityConfig[])
|
||||
: [];
|
||||
const hasTimeline = configEntities.some((entityConf) =>
|
||||
isTimelineEntity(
|
||||
computeDomain(entityConf.entity),
|
||||
this.hass!.states[entityConf.entity]
|
||||
)
|
||||
);
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${this._schema(this.hass.localize, hasTimeline)}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<hui-entity-editor
|
||||
@@ -313,21 +278,9 @@ export class HuiHistoryGraphCardEditor
|
||||
) as HistoryGraphCardConfig;
|
||||
}
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
schema.name === "label_position"
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.history-graph.label_position_helper"
|
||||
)
|
||||
: undefined;
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "show_names":
|
||||
case "label_position":
|
||||
case "logarithmic_scale":
|
||||
case "expand_legend":
|
||||
case "min_y_axis":
|
||||
|
||||
@@ -1206,6 +1206,8 @@ class HUIRoot extends LitElement {
|
||||
if (viewIndex !== this._curView) {
|
||||
const path = this.config.views[viewIndex].path || viewIndex;
|
||||
this._navigateToView(path);
|
||||
} else if (!this._editMode) {
|
||||
scrollTo({ behavior: "smooth", top: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,21 +22,6 @@ class HuiCastRow extends LitElement implements LovelaceRow {
|
||||
|
||||
@state() private _noHTTPS = false;
|
||||
|
||||
private _removeCastListeners?: (() => void)[];
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._castManager) {
|
||||
this._attachCastListeners(this._castManager);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._detachCastListeners();
|
||||
}
|
||||
|
||||
public setConfig(config: CastConfig): void {
|
||||
this._config = {
|
||||
icon: "mdi:television",
|
||||
@@ -102,9 +87,12 @@ class HuiCastRow extends LitElement implements LovelaceRow {
|
||||
getCastManager(this.hass.auth).then(
|
||||
(mgr) => {
|
||||
this._castManager = mgr;
|
||||
if (this.isConnected) {
|
||||
this._attachCastListeners(mgr);
|
||||
}
|
||||
mgr.addEventListener("connection-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
mgr.addEventListener("state-changed", () => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
},
|
||||
() => {
|
||||
this._castManager = null;
|
||||
@@ -124,22 +112,6 @@ class HuiCastRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
}
|
||||
|
||||
private _attachCastListeners(mgr: CastManager) {
|
||||
this._detachCastListeners();
|
||||
const update = () => {
|
||||
this.requestUpdate();
|
||||
};
|
||||
this._removeCastListeners = [
|
||||
mgr.addEventListener("connection-changed", update),
|
||||
mgr.addEventListener("state-changed", update),
|
||||
];
|
||||
}
|
||||
|
||||
private _detachCastListeners() {
|
||||
this._removeCastListeners?.forEach((remove) => remove());
|
||||
this._removeCastListeners = undefined;
|
||||
}
|
||||
|
||||
private async _sendLovelace() {
|
||||
await ensureConnectedCastSession(this._castManager!, this.hass.auth);
|
||||
castSendShowLovelaceView(
|
||||
|
||||
@@ -85,9 +85,10 @@ export class HUIViewBackground extends LitElement {
|
||||
|
||||
private _applyTheme() {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const themeBackground =
|
||||
computedStyles.getPropertyValue("--lovelace-background") ||
|
||||
computedStyles.getPropertyValue("--ha-panel-background");
|
||||
const themeBackground = computedStyles.getPropertyValue(
|
||||
"--lovelace-background"
|
||||
);
|
||||
|
||||
const fixedBackground = this._isFixedBackground(
|
||||
this.background || themeBackground
|
||||
);
|
||||
@@ -200,10 +201,7 @@ export class HUIViewBackground extends LitElement {
|
||||
width: 100%;
|
||||
background: var(
|
||||
--view-background,
|
||||
var(
|
||||
--lovelace-background,
|
||||
var(--ha-panel-background, var(--primary-background-color))
|
||||
)
|
||||
var(--lovelace-background, var(--primary-background-color))
|
||||
);
|
||||
opacity: var(--view-background-opacity);
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ export class HaMarketplaceWarning extends LitElement {
|
||||
<div class="box agree">
|
||||
<ha-checkbox
|
||||
autofocus
|
||||
variant="warning"
|
||||
.checked=${this._understood}
|
||||
.disabled=${this._accepting}
|
||||
@change=${this._understoodChanged}
|
||||
@@ -134,7 +133,6 @@ export class HaMarketplaceWarning extends LitElement {
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
variant="warning"
|
||||
.disabled=${!this._understood}
|
||||
.loading=${this._accepting}
|
||||
@click=${this._accept}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { consumeEntityState } from "../../../common/decorators/consume-context-entry";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
@@ -40,10 +39,9 @@ import {
|
||||
apiContext,
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../../data/context";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import { latestVersionIsSkipped } from "../../../data/update";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
@@ -149,9 +147,8 @@ export class HaMarketplaceRepositoryDashboard extends LitElement {
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["_repository", "update_entity_id"] })
|
||||
private _updateEntity?: UpdateEntity;
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
@state() private _repository?: RepositoryInfo;
|
||||
|
||||
@@ -604,11 +601,7 @@ export class HaMarketplaceRepositoryDashboard extends LitElement {
|
||||
}
|
||||
|
||||
// Installing and updating are the main actions, a reinstall stays in the menu
|
||||
const updateSkipped =
|
||||
this._updateEntity !== undefined &&
|
||||
latestVersionIsSkipped(this._updateEntity);
|
||||
|
||||
if (repository.pending_upgrade && !updateSkipped) {
|
||||
if (repository.pending_upgrade) {
|
||||
return {
|
||||
kind: "update",
|
||||
icon: mdiArrowUpBoldCircleOutline,
|
||||
@@ -637,43 +630,12 @@ export class HaMarketplaceRepositoryDashboard extends LitElement {
|
||||
detail: localize("ui.panel.marketplace.repository.next_step.restart", {
|
||||
name,
|
||||
}),
|
||||
action: html`<ha-button
|
||||
variant="warning"
|
||||
appearance="filled"
|
||||
@click=${this._restart}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
action: html`<ha-button appearance="filled" @click=${this._restart}>
|
||||
${localize("ui.panel.marketplace.repository.next_step.restart_button")}
|
||||
</ha-button>`,
|
||||
};
|
||||
}
|
||||
|
||||
// A skipped update can still be installed or unskipped from its entity
|
||||
if (repository.pending_upgrade) {
|
||||
return {
|
||||
kind: "installed",
|
||||
icon: mdiCheckCircle,
|
||||
title: localize(
|
||||
"ui.panel.marketplace.repository_status.update_skipped"
|
||||
),
|
||||
detail: localize("ui.panel.marketplace.repository.version_skipped", {
|
||||
installed: repository.installed_version,
|
||||
version: repository.available_version,
|
||||
}),
|
||||
action: html`<ha-button
|
||||
variant="neutral"
|
||||
appearance="outlined"
|
||||
@click=${this._update}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiArrowUpBoldCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${localize("ui.common.update")}
|
||||
</ha-button>`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "installed",
|
||||
icon: mdiCheckCircle,
|
||||
@@ -959,10 +921,10 @@ type: module</pre>`,
|
||||
// Updates are installed from their update entity everywhere else too, the
|
||||
// install dialog is for when there is none
|
||||
private _update() {
|
||||
if (this._updateEntity) {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._updateEntity.entity_id,
|
||||
});
|
||||
const entityId = this._repository!.update_entity_id;
|
||||
|
||||
if (entityId && this._states[entityId]) {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,21 +31,6 @@ const marketplaceTableParams = {
|
||||
direction: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
// The devices table reads these from the link
|
||||
const devicesTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
status: "string?",
|
||||
floor: "string?",
|
||||
area: "string?",
|
||||
domain: "string?",
|
||||
label: "string?",
|
||||
config_entry: "string?",
|
||||
sub_entry: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
application_credentials: {
|
||||
redirect: "/config/application_credentials",
|
||||
@@ -235,7 +220,6 @@ export const getMyRedirects = (): Redirects => ({
|
||||
},
|
||||
devices: {
|
||||
redirect: "/config/devices/dashboard",
|
||||
params: devicesTableParams,
|
||||
},
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { PluginInput } from "fullcalendar";
|
||||
// @ts-ignore
|
||||
import skeletonStyles from "fullcalendar/skeleton.css";
|
||||
import classicThemePlugin from "fullcalendar/themes/classic";
|
||||
// @ts-ignore
|
||||
import classicThemeStyles from "fullcalendar/themes/classic/theme.css";
|
||||
import { css, unsafeCSS } from "lit";
|
||||
|
||||
// The classic theme plus the class hooks that fullCalendarStyles styles
|
||||
export const fullCalendarTheme: PluginInput = {
|
||||
name: "theme-home-assistant",
|
||||
deps: [classicThemePlugin],
|
||||
optionDefaults: {
|
||||
viewClass: "ha-fc-view",
|
||||
tableHeaderClass: "ha-fc-table-header",
|
||||
dayHeaderInnerClass: ({ inPopover }) =>
|
||||
inPopover ? "" : "ha-fc-day-header-inner",
|
||||
},
|
||||
};
|
||||
|
||||
export const fullCalendarStyles = [
|
||||
unsafeCSS(skeletonStyles),
|
||||
unsafeCSS(classicThemeStyles),
|
||||
css`
|
||||
/* FullCalendar declares these on :root, which a shadow root never
|
||||
matches. Its popover and drag mirror sit outside the calendar element,
|
||||
so they go on the host. */
|
||||
:host {
|
||||
--fc-sticky-header-footer-z: 3;
|
||||
--fc-popover-z: 4;
|
||||
--fc-classic-primary: var(--primary-color);
|
||||
--fc-classic-primary-foreground: var(--text-primary-color);
|
||||
--fc-classic-event: #3788d8;
|
||||
--fc-classic-event-contrast: #fff;
|
||||
--fc-classic-background-event: var(--primary-color);
|
||||
--fc-classic-background-event-opacity: 15%;
|
||||
--fc-classic-background-event-foreground-opacity: 50%;
|
||||
--fc-classic-highlight: rgba(188, 232, 241, 0.3);
|
||||
--fc-classic-today: rgba(255, 220, 40, 0.15);
|
||||
--fc-classic-now: var(--error-color);
|
||||
--fc-classic-small-dot-width: 8px;
|
||||
--fc-classic-large-dot-width: 10px;
|
||||
--fc-classic-background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
--fc-classic-faint: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
--fc-classic-muted: rgba(var(--rgb-primary-text-color), 0.08);
|
||||
--fc-classic-strong: rgba(var(--rgb-primary-text-color), 0.14);
|
||||
--fc-classic-foreground: var(--primary-text-color);
|
||||
--fc-classic-faint-foreground: var(--disabled-text-color);
|
||||
--fc-classic-muted-foreground: var(--secondary-text-color);
|
||||
--fc-classic-border: var(--divider-color);
|
||||
--fc-classic-strong-border: var(--divider-color);
|
||||
}
|
||||
|
||||
.ha-fc-view {
|
||||
border-width: var(--calendar-border-width, 1px);
|
||||
border-radius: var(--calendar-border-radius, var(--mdc-shape-small, 4px));
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.ha-fc-view * {
|
||||
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.ha-fc-view *::-webkit-scrollbar {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
}
|
||||
|
||||
.ha-fc-view *::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
/* Sticky with an automatic height, so the header color needs an opaque
|
||||
background below it */
|
||||
.ha-fc-table-header {
|
||||
background: linear-gradient(
|
||||
var(--table-header-background-color),
|
||||
var(--table-header-background-color)
|
||||
)
|
||||
var(--fc-classic-background);
|
||||
}
|
||||
|
||||
.ha-fc-day-header-inner {
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -1,23 +0,0 @@
|
||||
import Decimal from "decimal.js";
|
||||
|
||||
// Like @formatjs/bigdecimal: never exponent notation, and log(10) from
|
||||
// Math.log10, as the precise log is slow and NumberFormat calls it per value
|
||||
const BigDecimal = Decimal.clone({
|
||||
precision: 40,
|
||||
toExpNeg: -9e15,
|
||||
toExpPos: 9e15,
|
||||
});
|
||||
|
||||
// Clones share this prototype; this shim is the only decimal.js user
|
||||
const { log } = BigDecimal.prototype;
|
||||
BigDecimal.prototype.log = function (this: Decimal, base?: Decimal.Value) {
|
||||
const value = this.toNumber();
|
||||
if (base !== 10 || !(value > 0) || !Number.isFinite(value)) {
|
||||
return log.call(this, base);
|
||||
}
|
||||
// Math.log10 rounds values just below a power of ten up to it
|
||||
const result = Math.log10(value);
|
||||
return new BigDecimal(result < this.e + 1 ? result : this.e + 0.5);
|
||||
};
|
||||
|
||||
export { BigDecimal, BigDecimal as Decimal, BigDecimal as default };
|
||||
@@ -18,9 +18,6 @@ let polyfilled = false;
|
||||
|
||||
const _polyfillTimeZoneData = polyfillTimeZoneData;
|
||||
|
||||
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
|
||||
const hasBigInt = typeof BigInt !== "undefined";
|
||||
|
||||
const polyfillIntl = async () => {
|
||||
if (polyfilled) {
|
||||
return;
|
||||
@@ -34,14 +31,14 @@ const polyfillIntl = async () => {
|
||||
if (shouldPolyfillLocale()) {
|
||||
await import("@formatjs/intl-locale/polyfill-force");
|
||||
}
|
||||
if (hasBigInt && shouldPolyfillDateTimeFormat(locale)) {
|
||||
if (shouldPolyfillDateTimeFormat(locale)) {
|
||||
polyfills.push(
|
||||
import("@formatjs/intl-datetimeformat/polyfill-force").then(() =>
|
||||
_polyfillTimeZoneData()
|
||||
)
|
||||
);
|
||||
}
|
||||
if (hasBigInt && shouldPolyfillDurationFormat()) {
|
||||
if (shouldPolyfillDurationFormat()) {
|
||||
polyfills.push(import("@formatjs/intl-durationformat/polyfill-force"));
|
||||
}
|
||||
if (shouldPolyfillDisplayNames(locale)) {
|
||||
|
||||
@@ -322,7 +322,6 @@
|
||||
"update": {
|
||||
"installing": "Installing",
|
||||
"installing_with_progress": "Installing ({progress}%)",
|
||||
"preparing": "Preparing",
|
||||
"up_to_date": "Up-to-date"
|
||||
},
|
||||
"text": {
|
||||
@@ -779,13 +778,6 @@
|
||||
"time_pattern": "time pattern",
|
||||
"homeassistant_stopping": "Home Assistant stopping",
|
||||
"homeassistant_starting": "Home Assistant starting",
|
||||
"cause_filter": {
|
||||
"caption": "Triggered by",
|
||||
"automation": "Automations",
|
||||
"script": "Scripts",
|
||||
"integration": "Integrations",
|
||||
"other": "Other"
|
||||
},
|
||||
"cause": {
|
||||
"by": "By {name}",
|
||||
"by_automation": "By automation: {name}",
|
||||
@@ -1876,7 +1868,6 @@
|
||||
"backups_link": "View backups"
|
||||
},
|
||||
"install_failed": "The update could not be started: {error}",
|
||||
"supervisor_logs": "[%key:ui::panel::config::logs::supervisor_logs%]",
|
||||
"create_backup": {
|
||||
"automatic": "Automatic backup before update",
|
||||
"automatic_description_last": "Last automatic backup {relative_time}.",
|
||||
@@ -4915,7 +4906,6 @@
|
||||
"custom_integration": "custom integration",
|
||||
"error_from_custom_integration": "This log entry is from a custom integration.",
|
||||
"show_full_logs": "Show raw logs",
|
||||
"supervisor_logs": "Supervisor logs",
|
||||
"show_condensed_logs": "Show condensed logs",
|
||||
"select_number_of_lines": "Select number of lines to download",
|
||||
"lines": "Lines",
|
||||
@@ -5572,7 +5562,7 @@
|
||||
"select_target": "Select a target",
|
||||
"home": "Home",
|
||||
"unassigned": "Unassigned",
|
||||
"other": "Other",
|
||||
"time_sun": "Time and sun",
|
||||
"blocks": "Blocks",
|
||||
"tabs": {
|
||||
"target": "By target",
|
||||
@@ -5678,9 +5668,7 @@
|
||||
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
||||
"start": "Event start",
|
||||
"end": "Event end",
|
||||
"offset": "Offset (optional)",
|
||||
"before": "Before",
|
||||
"after": "After",
|
||||
"offset": "Offset",
|
||||
"description": {
|
||||
"picker": "Triggers when a calendar event starts or ends.",
|
||||
"full": "When{offsetChoice, select, \n before { it's {offset} before}\n after { it's {offset} after}\n other {}\n} a calendar event{eventChoice, select, \n start { starts}\n end { ends}\n other { starts or ends}\n}{hasCalendar, select, \n true { in {calendar}}\n other {}\n}"
|
||||
@@ -5803,7 +5791,7 @@
|
||||
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
||||
"sunrise": "Sunrise",
|
||||
"sunset": "Sunset",
|
||||
"offset": "Offset in seconds or HH:MM:SS (optional)",
|
||||
"offset": "Offset",
|
||||
"description": {
|
||||
"picker": "Triggers when the sun sets or rises.",
|
||||
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
|
||||
@@ -7376,7 +7364,6 @@
|
||||
"setup_adapter_zwa2": "Connect ZWA-2",
|
||||
"setup_adapter_zwa2_description": "Adds a Z-Wave radio.",
|
||||
"setup_serial_configured": "Configured",
|
||||
"setup_serial_offline": "Offline",
|
||||
"setup_serial_port_ttl": "TTL",
|
||||
"setup_serial_port_rs232": "RS-232",
|
||||
"setup_serial_port_rs485": "RS-485",
|
||||
@@ -10667,12 +10654,6 @@
|
||||
"name": "History graph",
|
||||
"description": "This card allows you to display a graph for each of the entities listed.",
|
||||
"show_names": "Show entity names",
|
||||
"label_position": "Label position",
|
||||
"label_position_options": {
|
||||
"side": "Beside the graph",
|
||||
"below": "Below each bar"
|
||||
},
|
||||
"label_position_helper": "Applies only to entities shown as bars.",
|
||||
"logarithmic_scale": "Logarithmic scale",
|
||||
"expand_legend": "Expand legend",
|
||||
"min_y_axis": "Y axis minimum",
|
||||
@@ -12776,7 +12757,6 @@
|
||||
},
|
||||
"version_installed": "Version {version} installed",
|
||||
"version_update": "Version {installed} installed, {version} available",
|
||||
"version_skipped": "Version {installed} installed, {version} skipped",
|
||||
"requires_homeassistant": "The newest version needs Home Assistant {version} or newer.",
|
||||
"requires_newer_homeassistant": "The newest version needs a newer version of Home Assistant.",
|
||||
"earlier_version_hint": "Update Home Assistant, or select an earlier version with Install another version in the menu."
|
||||
|
||||
@@ -107,31 +107,6 @@ describe("QueryParamsController", () => {
|
||||
expect(navigated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("follows the router replacing the URL with its default page", () => {
|
||||
setUrl("/page/dashboard?other=1");
|
||||
window.dispatchEvent(
|
||||
Object.assign(new Event("location-changed"), {
|
||||
detail: { replace: true },
|
||||
})
|
||||
);
|
||||
expect(navigated).toHaveBeenCalledOnce();
|
||||
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page/dashboard?other=1&search=abc");
|
||||
});
|
||||
|
||||
it("does nothing once a link pushes a page below it", () => {
|
||||
setUrl("/page/item?other=1");
|
||||
window.dispatchEvent(
|
||||
Object.assign(new Event("location-changed"), {
|
||||
detail: { replace: false },
|
||||
})
|
||||
);
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page/item?other=1");
|
||||
expect(navigated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing when not enabled", () => {
|
||||
enabled = false;
|
||||
controller.write({ search: "abc" });
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import "@formatjs/intl-durationformat/polyfill-force";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DURATION_UNITS,
|
||||
formatDuration,
|
||||
formatDurationDigital,
|
||||
formatDurationLong,
|
||||
formatDurationNarrow,
|
||||
formatNumericDuration,
|
||||
} from "../../../src/common/datetime/format_duration";
|
||||
import type { HaDurationData } from "../../../src/components/ha-duration-input";
|
||||
import type { FrontendLocaleData } from "../../../src/data/translation";
|
||||
import {
|
||||
DateFormat,
|
||||
@@ -129,68 +127,3 @@ describe("formatNumericDuration", () => {
|
||||
expect(formatNumericDuration(LOCALE, duration)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without Intl.DurationFormat", () => {
|
||||
// Legacy browsers without BigInt cannot load the polyfill
|
||||
const DURATIONS: HaDurationData[] = [
|
||||
{ hours: 0, minutes: 0, seconds: 0 },
|
||||
{ seconds: 45 },
|
||||
{ hours: 1, minutes: 30 },
|
||||
{ days: 1, hours: 2, minutes: 3, seconds: 4 },
|
||||
{ hours: 0, minutes: 2, seconds: 3 },
|
||||
{ hours: 25, minutes: 0, seconds: 0 },
|
||||
{ hours: -1, minutes: -30, seconds: 0 },
|
||||
{ seconds: 1, milliseconds: 500 },
|
||||
{ milliseconds: 250 },
|
||||
{ days: 2, hours: 0, minutes: 0, seconds: 7 },
|
||||
// Rejected by Intl.DurationFormat
|
||||
{ hours: 1, minutes: -30 },
|
||||
{ seconds: 1.5 },
|
||||
{ hours: NaN, minutes: 5 },
|
||||
];
|
||||
|
||||
const attempt = (format: () => string) => {
|
||||
try {
|
||||
return format();
|
||||
} catch (err) {
|
||||
return (err as Error).name;
|
||||
}
|
||||
};
|
||||
|
||||
const formatAll = (locale: FrontendLocaleData) => [
|
||||
...DURATIONS.flatMap((duration) => [
|
||||
attempt(() => formatDurationLong(locale, duration)),
|
||||
attempt(() => formatDurationNarrow(locale, duration)),
|
||||
attempt(() => formatDurationDigital(locale, duration)),
|
||||
]),
|
||||
...["0", "0.25", "1", "95.5", "-1", "-0.5", "-1.25"].flatMap((value) =>
|
||||
DURATION_UNITS.map((unit) =>
|
||||
attempt(() => formatDuration(locale, value, unit))
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Intl, "DurationFormat")!;
|
||||
const expected = Object.fromEntries(
|
||||
["en", "de", "nl"].map((language) => [
|
||||
language,
|
||||
formatAll({ ...LOCALE, language }),
|
||||
])
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
Reflect.deleteProperty(Intl, "DurationFormat");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(Intl, "DurationFormat", descriptor);
|
||||
});
|
||||
|
||||
it.each(["en", "de", "nl"])(
|
||||
"matches Intl.DurationFormat in %s",
|
||||
(language) => {
|
||||
expect("DurationFormat" in Intl).toBe(false);
|
||||
expect(formatAll({ ...LOCALE, language })).toEqual(expected[language]);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
devicesFilterQueryParamConfig,
|
||||
devicesFilterQueryParams,
|
||||
devicesLinkFromUrl,
|
||||
} from "../../../src/common/url/devices-query-params";
|
||||
import { createQueryString } from "../../../src/common/url/query-params";
|
||||
|
||||
describe("devices query params", () => {
|
||||
it("reads nothing without table params", () => {
|
||||
expect(devicesLinkFromUrl("?historyBack=1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replaces every filter and the search with what a link asks for", () => {
|
||||
expect(devicesLinkFromUrl("?config_entry=abc&label=a,b")).toEqual({
|
||||
search: "",
|
||||
filters: {
|
||||
"ha-filter-states": { value: [], items: undefined },
|
||||
"ha-filter-floor-areas": { value: undefined, items: undefined },
|
||||
"ha-filter-integrations": { value: [], items: undefined },
|
||||
"ha-filter-labels": { value: ["a", "b"], items: undefined },
|
||||
config_entry: { value: ["abc"], items: undefined },
|
||||
sub_entry: { value: [], items: undefined },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("round trips the filters", () => {
|
||||
const filters = {
|
||||
"ha-filter-states": { value: ["disabled"], items: undefined },
|
||||
"ha-filter-floor-areas": {
|
||||
value: { floors: ["ground"], areas: ["kitchen"] },
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": { value: ["hue"], items: undefined },
|
||||
"ha-filter-labels": { value: ["a"], items: undefined },
|
||||
config_entry: { value: ["abc"], items: undefined },
|
||||
sub_entry: { value: ["def"], items: undefined },
|
||||
};
|
||||
|
||||
expect(
|
||||
devicesLinkFromUrl(
|
||||
createQueryString(
|
||||
devicesFilterQueryParams(filters),
|
||||
devicesFilterQueryParamConfig
|
||||
)
|
||||
)
|
||||
).toEqual({ search: "", filters });
|
||||
});
|
||||
|
||||
it("writes the values, not the items the filters found", () => {
|
||||
expect(
|
||||
devicesFilterQueryParams({
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: ["kitchen"] },
|
||||
items: new Set(["device_1"]),
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
status: undefined,
|
||||
floor: undefined,
|
||||
area: ["kitchen"],
|
||||
domain: undefined,
|
||||
label: undefined,
|
||||
config_entry: undefined,
|
||||
sub_entry: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -108,3 +108,83 @@ describe("normalizeAutomationConfig deprecated option reporting", () => {
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateAutomationConfig offset type", () => {
|
||||
const migrateTrigger = (trigger: Record<string, unknown>) =>
|
||||
(
|
||||
migrateAutomationConfig({ triggers: [trigger as any] }).triggers as any
|
||||
)[0];
|
||||
|
||||
it.each([
|
||||
["sun.sunrise", { hours: 1 }, { hours: -1 }],
|
||||
[
|
||||
"calendar.event_started",
|
||||
{ hours: 1, minutes: 30 },
|
||||
{ hours: -1, minutes: -30 },
|
||||
],
|
||||
["sun.sunset", { hours: -1 }, { hours: 1 }],
|
||||
[
|
||||
"sun.dawn",
|
||||
"00:30:00",
|
||||
{ hours: 0, minutes: -30, seconds: 0, milliseconds: 0 },
|
||||
],
|
||||
])(
|
||||
"folds a `before` offset type into the sign for %s",
|
||||
(trigger, offset, expected) => {
|
||||
expect(
|
||||
migrateTrigger({ trigger, options: { offset, offset_type: "before" } })
|
||||
).toEqual({ trigger, options: { offset: expected } });
|
||||
}
|
||||
);
|
||||
|
||||
it("drops an `after` offset type and keeps the offset", () => {
|
||||
expect(
|
||||
migrateTrigger({
|
||||
trigger: "calendar.event_ended",
|
||||
options: { offset: { hours: 1 }, offset_type: "after" },
|
||||
})
|
||||
).toEqual({
|
||||
trigger: "calendar.event_ended",
|
||||
options: { offset: { hours: 1 } },
|
||||
});
|
||||
});
|
||||
|
||||
it("drops the offset type when there is no offset", () => {
|
||||
expect(
|
||||
migrateTrigger({
|
||||
trigger: "sun.sunrise",
|
||||
options: { offset_type: "before" },
|
||||
})
|
||||
).toEqual({ trigger: "sun.sunrise", options: {} });
|
||||
});
|
||||
|
||||
it("leaves an offset it cannot read untouched", () => {
|
||||
const options = { offset: "{{ offset }}", offset_type: "before" };
|
||||
expect(
|
||||
migrateTrigger({ trigger: "sun.sunrise", options: { ...options } })
|
||||
).toEqual({ trigger: "sun.sunrise", options });
|
||||
});
|
||||
|
||||
it("leaves other integrations untouched", () => {
|
||||
const options = { offset: { hours: 1 }, offset_type: "before" };
|
||||
expect(
|
||||
migrateTrigger({ trigger: "custom.event", options: { ...options } })
|
||||
).toEqual({ trigger: "custom.event", options });
|
||||
});
|
||||
|
||||
it("does not flag the migration as deprecated", () => {
|
||||
const report: AutomationMigrationReport = { deprecated: false };
|
||||
migrateAutomationConfig(
|
||||
{
|
||||
triggers: [
|
||||
{
|
||||
trigger: "sun.sunrise",
|
||||
options: { offset: { hours: 1 }, offset_type: "before" },
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
report
|
||||
);
|
||||
expect(report.deprecated).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getCalendarColors,
|
||||
normalizeSubscriptionEventData,
|
||||
} from "../../src/data/calendar";
|
||||
import { getCalendarColors } from "../../src/data/calendar";
|
||||
|
||||
const style = {
|
||||
getPropertyValue: (prop: string) => (prop === "--color-1" ? "#4269d0" : ""),
|
||||
@@ -59,13 +56,3 @@ describe("getCalendarColors", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeSubscriptionEventData", () => {
|
||||
it("leaves the event text color empty when the calendar has none", () => {
|
||||
const event = normalizeSubscriptionEventData(
|
||||
{ summary: "Event", start: "2026-10-15", end: "2026-10-16" },
|
||||
{ entity_id: "calendar.home", backgroundColor: "rgba(0, 0, 0, 0.1)" }
|
||||
);
|
||||
expect(event?.contrastColor).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,5 @@
|
||||
import { describe, it, assert, expect, vi } from "vitest";
|
||||
import { computeDomain } from "../../src/common/entity/compute_domain";
|
||||
import {
|
||||
HistoryStream,
|
||||
computeHistory,
|
||||
isTimelineEntity,
|
||||
} from "../../src/data/history";
|
||||
import { HistoryStream, computeHistory } from "../../src/data/history";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import { mockDevice, mockEntity } from "../common/entity/context/context-mock";
|
||||
import {
|
||||
@@ -263,45 +258,3 @@ describe("computeHistory entity names", () => {
|
||||
expect(result.timeline[0]?.name).toBe("Caméra Allée ◂ Battery state");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTimelineEntity", () => {
|
||||
it("matches how computeHistory splits timeline and line entities", () => {
|
||||
const states = Object.fromEntries(
|
||||
(
|
||||
[
|
||||
["binary_sensor.door", "on", {}],
|
||||
["light.hallway", "off", {}],
|
||||
["sensor.power", "12", { unit_of_measurement: "W" }],
|
||||
["counter.visits", "3", {}],
|
||||
["climate.bedroom", "heat", {}],
|
||||
["humidifier.bedroom", "on", {}],
|
||||
["water_heater.tank", "eco", {}],
|
||||
["zone.home", "1", {}],
|
||||
] as const
|
||||
).map(([entityId, state, attributes]) => [
|
||||
entityId,
|
||||
createMockEntityState(entityId, state, attributes),
|
||||
])
|
||||
);
|
||||
const result = computeHistory(
|
||||
createMockHass(states),
|
||||
Object.fromEntries(
|
||||
Object.values(states).map((stateObj) => [
|
||||
stateObj.entity_id,
|
||||
[{ s: stateObj.state, a: stateObj.attributes, lu: 1_700_000_000 }],
|
||||
])
|
||||
),
|
||||
[],
|
||||
mockLocalize
|
||||
);
|
||||
const timelineIds = result.timeline.map((entity) => entity.entity_id);
|
||||
|
||||
for (const [entityId, stateObj] of Object.entries(states)) {
|
||||
expect(
|
||||
isTimelineEntity(computeDomain(entityId), stateObj),
|
||||
entityId
|
||||
).toBe(timelineIds.includes(entityId));
|
||||
}
|
||||
expect(timelineIds).toEqual(["binary_sensor.door", "light.hallway"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ForDict } from "../../src/data/automation";
|
||||
import { describeTrigger } from "../../src/data/automation_i18n";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
TimeZone,
|
||||
} from "../../src/data/translation";
|
||||
import en from "../../src/translations/en.json";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
type TranslationNode = string | { [key: string]: TranslationNode };
|
||||
|
||||
const localize = (key: string, values?: Record<string, unknown>) => {
|
||||
const message = key
|
||||
.split(".")
|
||||
.reduce<TranslationNode | undefined>(
|
||||
(translations, part) =>
|
||||
typeof translations === "object" ? translations[part] : undefined,
|
||||
en as TranslationNode
|
||||
);
|
||||
return typeof message === "string"
|
||||
? (new IntlMessageFormat(message, "en").format(values) as string)
|
||||
: "";
|
||||
};
|
||||
|
||||
const hass = {
|
||||
localize,
|
||||
locale: {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
},
|
||||
config: { time_zone: "Etc/UTC" },
|
||||
states: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
describe("legacy calendar trigger offset description", () => {
|
||||
it.each([
|
||||
[
|
||||
"-01:30:00",
|
||||
"When it's 1 hour, 30 minutes before a calendar event starts",
|
||||
],
|
||||
[
|
||||
{ hours: 1, minutes: -30 },
|
||||
"When it's 30 minutes after a calendar event starts",
|
||||
],
|
||||
[
|
||||
{ seconds: 1.5 },
|
||||
"When it's 1 second, 500 milliseconds after a calendar event starts",
|
||||
],
|
||||
[{ hours: 0, minutes: 0, seconds: 0 }, "When a calendar event starts"],
|
||||
])("describes %j", (offset, expected) => {
|
||||
expect(
|
||||
describeTrigger(
|
||||
{
|
||||
trigger: "calendar",
|
||||
event: "start",
|
||||
entity_id: "",
|
||||
offset: offset as string | ForDict,
|
||||
},
|
||||
hass,
|
||||
[]
|
||||
)
|
||||
).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("legacy sun trigger offset description", () => {
|
||||
it.each([
|
||||
[{ minutes: -30 }, "When the sun sets offset by -30:00"],
|
||||
[{ hours: 1, minutes: -30 }, "When the sun sets offset by +30:00"],
|
||||
[{ hours: 0, minutes: 0, seconds: 0 }, "When the sun sets"],
|
||||
])("describes %j", (offset, expected) => {
|
||||
expect(
|
||||
describeTrigger({ trigger: "sun", event: "sunset", offset }, hass, [])
|
||||
).toBe(expected);
|
||||
});
|
||||
});
|
||||
+1
-122
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getNestedStepConfig,
|
||||
getTracedEntityId,
|
||||
getTraceUrl,
|
||||
} from "../../src/data/trace";
|
||||
import { getTraceUrl } from "../../src/data/trace";
|
||||
|
||||
// Trace links from the logbook and from trace steps share this URL. Automation
|
||||
// ids are free-form, and the automation router decodes the path segment, so
|
||||
@@ -32,120 +28,3 @@ describe("getTraceUrl", () => {
|
||||
expect(parsed.searchParams.get("run_id")).toBe("run_1");
|
||||
});
|
||||
});
|
||||
|
||||
// The step details of a trace describe nested steps by following their trace
|
||||
// path through the config of the selected step. Core traces the conditions of
|
||||
// an `if` action as `if/condition/<index>`, while the config keeps them in `if`.
|
||||
describe("getNestedStepConfig", () => {
|
||||
const stateCondition = {
|
||||
condition: "state",
|
||||
entity_id: "input_boolean.a",
|
||||
state: "off",
|
||||
};
|
||||
const orCondition = { condition: "or", conditions: [stateCondition] };
|
||||
|
||||
it("follows the conditions of a choose option", () => {
|
||||
const config = { choose: [{ conditions: [orCondition], sequence: [] }] };
|
||||
|
||||
expect(getNestedStepConfig(config, "choose/0/conditions/0")).toBe(
|
||||
orCondition
|
||||
);
|
||||
expect(
|
||||
getNestedStepConfig(config, "choose/0/conditions/0/conditions/0")
|
||||
).toBe(stateCondition);
|
||||
});
|
||||
|
||||
it("follows the conditions of an if action", () => {
|
||||
const config = { if: [orCondition], then: [] };
|
||||
|
||||
expect(getNestedStepConfig(config, "if/condition/0")).toBe(orCondition);
|
||||
expect(getNestedStepConfig(config, "if/condition/0/conditions/0")).toBe(
|
||||
stateCondition
|
||||
);
|
||||
expect(
|
||||
getNestedStepConfig(config, "if/condition/0/conditions/0/entity_id/0")
|
||||
).toBe(stateCondition);
|
||||
});
|
||||
|
||||
it("follows an if action with a single condition", () => {
|
||||
const config = { if: stateCondition, then: [] };
|
||||
|
||||
expect(getNestedStepConfig(config, "if/condition/0")).toBe(stateCondition);
|
||||
});
|
||||
|
||||
it("follows nested conditions of a single condition", () => {
|
||||
const config = { if: orCondition, then: [] };
|
||||
|
||||
expect(getNestedStepConfig(config, "if/condition/0/conditions/0")).toBe(
|
||||
stateCondition
|
||||
);
|
||||
});
|
||||
|
||||
it("returns template conditions written as plain strings", () => {
|
||||
const template = "{{ is_state('light.a', 'off') }}";
|
||||
|
||||
expect(
|
||||
getNestedStepConfig({ if: [template], then: [] }, "if/condition/0")
|
||||
).toBe(template);
|
||||
expect(
|
||||
getNestedStepConfig({ if: template, then: [] }, "if/condition/0")
|
||||
).toBe(template);
|
||||
expect(
|
||||
getNestedStepConfig(
|
||||
{ choose: [{ conditions: [template], sequence: [] }] },
|
||||
"choose/0/conditions/0"
|
||||
)
|
||||
).toBe(template);
|
||||
});
|
||||
|
||||
it("stops at a state condition with a list of entities", () => {
|
||||
const condition = {
|
||||
condition: "state",
|
||||
entity_id: ["light.a", "light.b"],
|
||||
state: "off",
|
||||
};
|
||||
const config = { if: [condition], then: [] };
|
||||
|
||||
expect(getNestedStepConfig(config, "if/condition/0/entity_id/1")).toBe(
|
||||
condition
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTracedEntityId", () => {
|
||||
it("returns the entity checked by an entity_id step", () => {
|
||||
const condition = {
|
||||
condition: "state",
|
||||
entity_id: ["light.a", "light.b"],
|
||||
state: "off",
|
||||
};
|
||||
|
||||
expect(getTracedEntityId(condition, "if/condition/0/entity_id/1")).toBe(
|
||||
"light.b"
|
||||
);
|
||||
expect(
|
||||
getTracedEntityId(
|
||||
{ condition: "state", entity_id: "light.a", state: "off" },
|
||||
"entity_id/0"
|
||||
)
|
||||
).toBe("light.a");
|
||||
});
|
||||
|
||||
it("handles a single entity and indexes outside the list", () => {
|
||||
expect(getTracedEntityId({ entity_id: "light.a" }, "entity_id/1")).toBe(
|
||||
"light.a"
|
||||
);
|
||||
expect(
|
||||
getTracedEntityId({ entity_id: ["light.a"] }, "entity_id/1")
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores steps that are not entity_id steps", () => {
|
||||
const condition = { condition: "state", entity_id: "light.a" };
|
||||
|
||||
expect(getTracedEntityId(condition, "if/condition/0")).toBeUndefined();
|
||||
expect(
|
||||
getTracedEntityId("{{ true }}", "if/condition/0/entity_id/0")
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { HaDialog } from "../../../src/components/ha-dialog";
|
||||
import type {
|
||||
DataEntryFlowProgressedEvent,
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepExternal,
|
||||
DataEntryFlowStepForm,
|
||||
DataEntryFlowStepMenu,
|
||||
DataEntryFlowStepProgress,
|
||||
} from "../../../src/data/data_entry_flow";
|
||||
import "../../../src/dialogs/config-flow/dialog-data-entry-flow";
|
||||
import type { FlowConfig } from "../../../src/dialogs/config-flow/show-dialog-data-entry-flow";
|
||||
@@ -75,28 +72,6 @@ const menuStep: DataEntryFlowStepMenu = {
|
||||
menu_options: ["next"],
|
||||
};
|
||||
|
||||
const progressStep: DataEntryFlowStepProgress = {
|
||||
type: "progress",
|
||||
flow_id: "test-flow",
|
||||
handler: "test",
|
||||
step_id: "install",
|
||||
progress_action: "install",
|
||||
};
|
||||
|
||||
const externalStep: DataEntryFlowStepExternal = {
|
||||
type: "external",
|
||||
flow_id: "test-flow",
|
||||
handler: "test",
|
||||
step_id: "auth",
|
||||
url: "https://example.com/authorize",
|
||||
description_placeholders: {},
|
||||
};
|
||||
|
||||
const nextFormStep: DataEntryFlowStepForm = {
|
||||
...formStep,
|
||||
step_id: "finish",
|
||||
};
|
||||
|
||||
const flowConfig: FlowConfig = {
|
||||
flowType: "config_flow",
|
||||
showDevices: false,
|
||||
@@ -124,28 +99,10 @@ const flowConfig: FlowConfig = {
|
||||
renderLoadingDescription: () => "",
|
||||
};
|
||||
|
||||
const eventListeners: Record<string, (ev: unknown) => void> = {};
|
||||
|
||||
const hass = {
|
||||
localize: (key: string) => key,
|
||||
devices: {},
|
||||
connection: {
|
||||
subscribeEvents: vi.fn(
|
||||
async (callback: (ev: unknown) => void, eventType: string) => {
|
||||
eventListeners[eventType] = callback;
|
||||
return () => undefined;
|
||||
}
|
||||
),
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const fireFlowProgressed = (flowId = "test-flow") => {
|
||||
const event: DataEntryFlowProgressedEvent = {
|
||||
type: "data_entry_flow_progressed",
|
||||
data: { handler: "test", flow_id: flowId, refresh: false },
|
||||
};
|
||||
eventListeners.data_entry_flow_progressed(event);
|
||||
};
|
||||
} as HomeAssistant;
|
||||
|
||||
const nextTask = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
@@ -248,151 +205,3 @@ describe("dialog-data-entry-flow dismissal", () => {
|
||||
expect(innerDialog(dialog).preventScrimClose).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dialog-data-entry-flow progress", () => {
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(flowConfig.fetchFlow).mockReset().mockResolvedValue(formStep);
|
||||
});
|
||||
|
||||
const shownStep = (dialog: HTMLElementTagNameMap["dialog-data-entry-flow"]) =>
|
||||
dialog.shadowRoot!.querySelector<HTMLElement & { step: DataEntryFlowStep }>(
|
||||
"step-flow-form, step-flow-progress"
|
||||
)?.step;
|
||||
|
||||
const deferFetchFlow = () => {
|
||||
let resolveFetch!: (step: DataEntryFlowStep) => void;
|
||||
vi.mocked(flowConfig.fetchFlow).mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveFetch = resolve;
|
||||
})
|
||||
);
|
||||
return resolveFetch;
|
||||
};
|
||||
|
||||
const settle = async (
|
||||
dialog: HTMLElementTagNameMap["dialog-data-entry-flow"]
|
||||
) => {
|
||||
await nextTask();
|
||||
await dialog.updateComplete;
|
||||
await nextTask();
|
||||
await dialog.updateComplete;
|
||||
};
|
||||
|
||||
it("shows the next step when the progress task finished before subscribing", async () => {
|
||||
vi.mocked(flowConfig.fetchFlow).mockResolvedValueOnce(nextFormStep);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledOnce();
|
||||
expect(shownStep(dialog)).toEqual(nextFormStep);
|
||||
});
|
||||
|
||||
it("does not fetch an external step on subscribe", async () => {
|
||||
const open = vi.spyOn(window, "open").mockReturnValue(null);
|
||||
|
||||
const dialog = await openDialog(externalStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).not.toHaveBeenCalled();
|
||||
expect(open).toHaveBeenCalledOnce();
|
||||
open.mockRestore();
|
||||
});
|
||||
|
||||
it("shows the next task when a progress step keeps its step_id", async () => {
|
||||
const nextTaskStep: DataEntryFlowStepProgress = {
|
||||
...progressStep,
|
||||
progress_action: "configure",
|
||||
};
|
||||
vi.mocked(flowConfig.fetchFlow).mockResolvedValueOnce(nextTaskStep);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledOnce();
|
||||
expect(shownStep(dialog)).toEqual(nextTaskStep);
|
||||
});
|
||||
|
||||
it("retries the catch-up fetch when it fails after a progressed event", async () => {
|
||||
let rejectFetch!: (err: Error) => void;
|
||||
vi.mocked(flowConfig.fetchFlow)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectFetch = reject;
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce(nextFormStep);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
fireFlowProgressed();
|
||||
rejectFetch(new Error("connection lost"));
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledTimes(2);
|
||||
expect(shownStep(dialog)).toEqual(nextFormStep);
|
||||
});
|
||||
|
||||
it("keeps waiting when the catch-up fetch fails without a progressed event", async () => {
|
||||
vi.mocked(flowConfig.fetchFlow).mockRejectedValueOnce(
|
||||
new Error("connection lost")
|
||||
);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledOnce();
|
||||
expect(shownStep(dialog)).toEqual(progressStep);
|
||||
});
|
||||
|
||||
it("follows a progressed event after catching up", async () => {
|
||||
vi.mocked(flowConfig.fetchFlow)
|
||||
.mockResolvedValueOnce(progressStep)
|
||||
.mockResolvedValueOnce(nextFormStep);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
await settle(dialog);
|
||||
fireFlowProgressed();
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledTimes(2);
|
||||
expect(shownStep(dialog)).toEqual(nextFormStep);
|
||||
});
|
||||
|
||||
it("fetches the flow once when the progressed event arrives while catching up", async () => {
|
||||
const resolveFetch = deferFetchFlow();
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
fireFlowProgressed();
|
||||
resolveFetch(nextFormStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledOnce();
|
||||
expect(shownStep(dialog)).toEqual(nextFormStep);
|
||||
});
|
||||
|
||||
it("fetches again when the flow progressed after the catch-up fetch", async () => {
|
||||
const resolveFetch = deferFetchFlow();
|
||||
vi.mocked(flowConfig.fetchFlow).mockResolvedValueOnce(nextFormStep);
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
fireFlowProgressed();
|
||||
resolveFetch(progressStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(flowConfig.fetchFlow).toHaveBeenCalledTimes(2);
|
||||
expect(shownStep(dialog)).toEqual(nextFormStep);
|
||||
});
|
||||
|
||||
it("ignores the catch-up fetch once the dialog has closed", async () => {
|
||||
const resolveFetch = deferFetchFlow();
|
||||
|
||||
const dialog = await openDialog(progressStep);
|
||||
innerDialog(dialog).dispatchEvent(new CustomEvent("closed"));
|
||||
resolveFetch(nextFormStep);
|
||||
await settle(dialog);
|
||||
|
||||
expect(dialog["_step"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { scaleCoordinates } from "../../../../../src/panels/lovelace/common/graph/coordinates";
|
||||
|
||||
// hui-graph-base rescales the line on screen into a new size before it
|
||||
// animates to coordinates computed for that size. Wrong math here makes the
|
||||
// graph jump at the start of the animation after a resize.
|
||||
describe("scaleCoordinates", () => {
|
||||
it("scales x and y independently into the new size", () => {
|
||||
const scaled = scaleCoordinates(
|
||||
[
|
||||
[0, 60],
|
||||
[150, 30],
|
||||
[300, 0],
|
||||
],
|
||||
{ width: 300, height: 60 },
|
||||
{ width: 200, height: 80 }
|
||||
);
|
||||
const expected = [
|
||||
[0, 80],
|
||||
[100, 40],
|
||||
[200, 0],
|
||||
];
|
||||
expect(scaled).toHaveLength(expected.length);
|
||||
scaled.forEach(([x, y], index) => {
|
||||
expect(x).toBeCloseTo(expected[index][0]);
|
||||
expect(y).toBeCloseTo(expected[index][1]);
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the same coordinates when the size did not change", () => {
|
||||
const coordinates = [
|
||||
[0, 10],
|
||||
[300, 50],
|
||||
];
|
||||
expect(
|
||||
scaleCoordinates(
|
||||
coordinates,
|
||||
{ width: 300, height: 60 },
|
||||
{ width: 300, height: 60 }
|
||||
)
|
||||
).toBe(coordinates);
|
||||
});
|
||||
});
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user